---
.../src/installer/steps/permissionsreport.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Umbraco.Web.UI.Client/src/installer/steps/permissionsreport.html b/src/Umbraco.Web.UI.Client/src/installer/steps/permissionsreport.html
index f3067e8fb6..ae3d4ed49a 100644
--- a/src/Umbraco.Web.UI.Client/src/installer/steps/permissionsreport.html
+++ b/src/Umbraco.Web.UI.Client/src/installer/steps/permissionsreport.html
@@ -3,7 +3,7 @@
In order to run Umbraco, you'll need to update your permission settings.
Detailed information about the correct file and folder permissions for Umbraco can be found
- here.
+ here.
The following report list the permissions that are currently failing. Once the permissions are fixed press the 'Go back' button to restart the installation.
From 7bde16b4ef663306eb4edf306f23ccc443b8f74e Mon Sep 17 00:00:00 2001
From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
Date: Thu, 9 Nov 2023 14:18:34 +0100
Subject: [PATCH 08/36] V13: Add eventype to webhookevents (#15157)
* Refactor IWebhookEvent to contain event type.
* refactor frontend to filter on eventType.
* Display event names
* refactor to use eventNames
* remove npm from overview
* implement alias for WebhookEvents
* Implement [WebhookEvent] attribute
* Refactor IWebhookService to get by event alias and not name
* Rename parameter to fit method name
* to lower event type to avoid casing issues
* Apply suggestions from code review
Co-authored-by: Ronald Barendse
* Change event names from constants to hard coded. And give more friendly names
* Refactor to not use event names, where it was not intended
* Add renaming column migration
* display event alias in logs
* Update migration to check if old column is there
* Apply suggestions from code review
Co-authored-by: Kenn Jacobsen
* add determineResource function to avoid duplicate code
---------
Co-authored-by: Zeegaan
Co-authored-by: Ronald Barendse
Co-authored-by: Kenn Jacobsen
---
src/Umbraco.Core/Constants-WebhookEvents.cs | 66 +++++++++++++------
src/Umbraco.Core/Models/WebhookLog.cs | 2 +-
.../Repositories/IWebhookRepository.cs | 8 ++-
src/Umbraco.Core/Services/IWebHookService.cs | 2 +-
.../Services/IWebhookFiringService.cs | 2 +-
.../Services/IWebhookLogFactory.cs | 2 +-
.../Services/WebhookLogFactory.cs | 4 +-
src/Umbraco.Core/Services/WebhookService.cs | 4 +-
.../Events/ContentDeleteWebhookEvent.cs | 6 +-
.../Events/ContentPublishWebhookEvent.cs | 6 +-
.../Events/ContentUnpublishWebhookEvent.cs | 6 +-
.../Events/MediaDeleteWebhookEvent.cs | 6 +-
.../Webhooks/Events/MediaSaveWebhookEvent.cs | 6 +-
src/Umbraco.Core/Webhooks/IWebhookEvent.cs | 4 ++
.../Webhooks/WebhookEventAttribute.cs | 26 ++++++++
src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 26 ++++++--
.../Webhooks/WebhookEventContentBase.cs | 7 +-
.../Migrations/Upgrade/UmbracoPlan.cs | 1 +
.../Upgrade/V_13_0_0/RenameEventNameColumn.cs | 28 ++++++++
.../Persistence/Dtos/WebhookLogDto.cs | 4 +-
.../Factories/WebhookLogFactory.cs | 4 +-
.../Implement/WebhookRepository.cs | 4 +-
.../Implement/WebhookFiringService.cs | 10 +--
.../Controllers/WebhookController.cs | 7 +-
.../UmbracoBuilderExtensions.cs | 1 +
.../Mapping/WebhookMapDefinition.cs | 19 ++----
.../Services/IWebhookPresentationFactory.cs | 10 +++
.../Services/WebhookPresentationFactory.cs | 37 +++++++++++
.../Models/WebhookEventViewModel.cs | 7 ++
.../Models/WebhookLogViewModel.cs | 4 +-
.../Models/WebhookViewModel.cs | 2 +-
.../eventpicker/eventpicker.controller.js | 28 ++++----
.../eventpicker/eventpicker.html | 2 +-
.../src/views/webhooks/logs.html | 2 +-
.../src/views/webhooks/overlays/details.html | 2 +-
.../webhooks/overlays/edit.controller.js | 30 ++++++---
.../src/views/webhooks/overlays/edit.html | 6 +-
.../src/views/webhooks/webhooks.controller.js | 37 ++++++++---
.../Services/WebhookLogServiceTests.cs | 4 +-
.../Services/WebhookServiceTests.cs | 42 ++++++------
40 files changed, 329 insertions(+), 145 deletions(-)
create mode 100644 src/Umbraco.Core/Webhooks/WebhookEventAttribute.cs
create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/RenameEventNameColumn.cs
create mode 100644 src/Umbraco.Web.BackOffice/Services/IWebhookPresentationFactory.cs
create mode 100644 src/Umbraco.Web.BackOffice/Services/WebhookPresentationFactory.cs
diff --git a/src/Umbraco.Core/Constants-WebhookEvents.cs b/src/Umbraco.Core/Constants-WebhookEvents.cs
index 24fe890221..afd57b5188 100644
--- a/src/Umbraco.Core/Constants-WebhookEvents.cs
+++ b/src/Umbraco.Core/Constants-WebhookEvents.cs
@@ -4,29 +4,55 @@ public static partial class Constants
{
public static class WebhookEvents
{
- ///
- /// Webhook event name for content publish.
- ///
- public const string ContentPublish = "ContentPublish";
+ public static class Aliases
+ {
+ ///
+ /// Webhook event alias for content publish.
+ ///
+ public const string ContentPublish = "Umbraco.ContentPublish";
- ///
- /// Webhook event name for content delete.
- ///
- public const string ContentDelete = "ContentDelete";
+ ///
+ /// Webhook event alias for content delete.
+ ///
+ public const string ContentDelete = "Umbraco.ContentDelete";
- ///
- /// Webhook event name for content unpublish.
- ///
- public const string ContentUnpublish = "ContentUnpublish";
+ ///
+ /// Webhook event alias for content unpublish.
+ ///
+ public const string ContentUnpublish = "Umbraco.ContentUnpublish";
- ///
- /// Webhook event name for media delete.
- ///
- public const string MediaDelete = "MediaDelete";
+ ///
+ /// Webhook event alias for media delete.
+ ///
+ public const string MediaDelete = "Umbraco.MediaDelete";
- ///
- /// Webhook event name for media save.
- ///
- public const string MediaSave = "MediaSave";
+ ///
+ /// Webhook event alias for media save.
+ ///
+ public const string MediaSave = "Umbraco.MediaSave";
+ }
+
+ public static class Types
+ {
+ ///
+ /// Webhook event type for content.
+ ///
+ public const string Content = "Content";
+
+ ///
+ /// Webhook event type for content media.
+ ///
+ public const string Media = "Media";
+
+ ///
+ /// Webhook event type for content member.
+ ///
+ public const string Member = "Member";
+
+ ///
+ /// Webhook event type for others, this is the default category if you have not chosen one.
+ ///
+ public const string Other = "Other";
+ }
}
}
diff --git a/src/Umbraco.Core/Models/WebhookLog.cs b/src/Umbraco.Core/Models/WebhookLog.cs
index bd37d79165..e65abdf990 100644
--- a/src/Umbraco.Core/Models/WebhookLog.cs
+++ b/src/Umbraco.Core/Models/WebhookLog.cs
@@ -14,7 +14,7 @@ public class WebhookLog
public DateTime Date { get; set; }
- public string EventName { get; set; } = string.Empty;
+ public string EventAlias { get; set; } = string.Empty;
public int RetryCount { get; set; }
diff --git a/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs
index d045cd172f..3013ee59e0 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IWebhookRepository.cs
@@ -29,9 +29,11 @@ public interface IWebhookRepository
///
/// Gets a webhook by key
///
- /// The key of the webhook which will be retrieved.
- /// The webhook with the given key.
- Task> GetByEventNameAsync(string eventName);
+ /// The alias of an event, which is referenced by a webhook.
+ ///
+ /// A paged model of
+ ///
+ Task> GetByAliasAsync(string alias);
///
/// Gets a webhook by key
diff --git a/src/Umbraco.Core/Services/IWebHookService.cs b/src/Umbraco.Core/Services/IWebHookService.cs
index 84e5319fe1..785f5e8701 100644
--- a/src/Umbraco.Core/Services/IWebHookService.cs
+++ b/src/Umbraco.Core/Services/IWebHookService.cs
@@ -36,5 +36,5 @@ public interface IWebHookService
///
/// Gets webhooks by event name.
///
- Task> GetByEventNameAsync(string eventName);
+ Task> GetByAliasAsync(string alias);
}
diff --git a/src/Umbraco.Core/Services/IWebhookFiringService.cs b/src/Umbraco.Core/Services/IWebhookFiringService.cs
index 0482290c3d..53a631bff3 100644
--- a/src/Umbraco.Core/Services/IWebhookFiringService.cs
+++ b/src/Umbraco.Core/Services/IWebhookFiringService.cs
@@ -4,5 +4,5 @@ namespace Umbraco.Cms.Core.Services;
public interface IWebhookFiringService
{
- Task FireAsync(Webhook webhook, string eventName, object? payload, CancellationToken cancellationToken);
+ Task FireAsync(Webhook webhook, string eventAlias, object? payload, CancellationToken cancellationToken);
}
diff --git a/src/Umbraco.Core/Services/IWebhookLogFactory.cs b/src/Umbraco.Core/Services/IWebhookLogFactory.cs
index fa600dda82..3f586f5da4 100644
--- a/src/Umbraco.Core/Services/IWebhookLogFactory.cs
+++ b/src/Umbraco.Core/Services/IWebhookLogFactory.cs
@@ -5,5 +5,5 @@ namespace Umbraco.Cms.Core.Services;
public interface IWebhookLogFactory
{
- Task CreateAsync(string eventName, WebhookResponseModel responseModel, Webhook webhook, CancellationToken cancellationToken);
+ Task CreateAsync(string eventAlias, WebhookResponseModel responseModel, Webhook webhook, CancellationToken cancellationToken);
}
diff --git a/src/Umbraco.Core/Services/WebhookLogFactory.cs b/src/Umbraco.Core/Services/WebhookLogFactory.cs
index 22dd75fe84..455bc45e27 100644
--- a/src/Umbraco.Core/Services/WebhookLogFactory.cs
+++ b/src/Umbraco.Core/Services/WebhookLogFactory.cs
@@ -5,12 +5,12 @@ namespace Umbraco.Cms.Core.Services;
public class WebhookLogFactory : IWebhookLogFactory
{
- public async Task CreateAsync(string eventName, WebhookResponseModel responseModel, Webhook webhook, CancellationToken cancellationToken)
+ public async Task CreateAsync(string eventAlias, WebhookResponseModel responseModel, Webhook webhook, CancellationToken cancellationToken)
{
var log = new WebhookLog
{
Date = DateTime.UtcNow,
- EventName = eventName,
+ EventAlias = eventAlias,
Key = Guid.NewGuid(),
Url = webhook.Url,
WebhookKey = webhook.Key,
diff --git a/src/Umbraco.Core/Services/WebhookService.cs b/src/Umbraco.Core/Services/WebhookService.cs
index 9813199db8..424f1afb14 100644
--- a/src/Umbraco.Core/Services/WebhookService.cs
+++ b/src/Umbraco.Core/Services/WebhookService.cs
@@ -80,10 +80,10 @@ public class WebhookService : IWebHookService
}
///
- public async Task> GetByEventNameAsync(string eventName)
+ public async Task> GetByAliasAsync(string alias)
{
using ICoreScope scope = _provider.CreateCoreScope();
- PagedModel webhooks = await _webhookRepository.GetByEventNameAsync(eventName);
+ PagedModel webhooks = await _webhookRepository.GetByAliasAsync(alias);
scope.Complete();
return webhooks.Items;
diff --git a/src/Umbraco.Core/Webhooks/Events/ContentDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/ContentDeleteWebhookEvent.cs
index 52b8d233e5..d8386b9914 100644
--- a/src/Umbraco.Core/Webhooks/Events/ContentDeleteWebhookEvent.cs
+++ b/src/Umbraco.Core/Webhooks/Events/ContentDeleteWebhookEvent.cs
@@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Webhooks.Events;
+[WebhookEvent("Content was deleted", Constants.WebhookEvents.Types.Content)]
public class ContentDeleteWebhookEvent : WebhookEventContentBase
{
public ContentDeleteWebhookEvent(
@@ -18,11 +19,12 @@ public class ContentDeleteWebhookEvent : WebhookEventContentBase Constants.WebhookEvents.Aliases.ContentUnpublish;
+
protected override IEnumerable GetEntitiesFromNotification(ContentDeletedNotification notification) =>
notification.DeletedEntities;
diff --git a/src/Umbraco.Core/Webhooks/Events/ContentPublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/ContentPublishWebhookEvent.cs
index 8f308432b8..03f2e71706 100644
--- a/src/Umbraco.Core/Webhooks/Events/ContentPublishWebhookEvent.cs
+++ b/src/Umbraco.Core/Webhooks/Events/ContentPublishWebhookEvent.cs
@@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Webhooks.Events;
+[WebhookEvent("Content was published", Constants.WebhookEvents.Types.Content)]
public class ContentPublishWebhookEvent : WebhookEventContentBase
{
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
@@ -26,13 +27,14 @@ public class ContentPublishWebhookEvent : WebhookEventContentBase Constants.WebhookEvents.Aliases.ContentPublish;
+
protected override IEnumerable GetEntitiesFromNotification(ContentPublishedNotification notification) => notification.PublishedEntities;
protected override object? ConvertEntityToRequestPayload(IContent entity)
diff --git a/src/Umbraco.Core/Webhooks/Events/ContentUnpublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/ContentUnpublishWebhookEvent.cs
index c8a8fd789e..354cdb295b 100644
--- a/src/Umbraco.Core/Webhooks/Events/ContentUnpublishWebhookEvent.cs
+++ b/src/Umbraco.Core/Webhooks/Events/ContentUnpublishWebhookEvent.cs
@@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Webhooks.Events;
+[WebhookEvent("Content was unpublished", Constants.WebhookEvents.Types.Content)]
public class ContentUnpublishWebhookEvent : WebhookEventContentBase
{
public ContentUnpublishWebhookEvent(
@@ -18,11 +19,12 @@ public class ContentUnpublishWebhookEvent : WebhookEventContentBase Constants.WebhookEvents.Aliases.ContentDelete;
+
protected override IEnumerable GetEntitiesFromNotification(ContentUnpublishedNotification notification) => notification.UnpublishedEntities;
protected override object ConvertEntityToRequestPayload(IContent entity) => new DefaultPayloadModel { Id = entity.Key };
diff --git a/src/Umbraco.Core/Webhooks/Events/MediaDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/MediaDeleteWebhookEvent.cs
index eb19e3e888..ab0fd98942 100644
--- a/src/Umbraco.Core/Webhooks/Events/MediaDeleteWebhookEvent.cs
+++ b/src/Umbraco.Core/Webhooks/Events/MediaDeleteWebhookEvent.cs
@@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Webhooks.Events;
+[WebhookEvent("Media was deleted", Constants.WebhookEvents.Types.Media)]
public class MediaDeleteWebhookEvent : WebhookEventContentBase
{
public MediaDeleteWebhookEvent(
@@ -18,11 +19,12 @@ public class MediaDeleteWebhookEvent : WebhookEventContentBase Constants.WebhookEvents.Aliases.MediaDelete;
+
protected override IEnumerable GetEntitiesFromNotification(MediaDeletedNotification notification) => notification.DeletedEntities;
protected override object ConvertEntityToRequestPayload(IMedia entity) => new DefaultPayloadModel { Id = entity.Key };
diff --git a/src/Umbraco.Core/Webhooks/Events/MediaSaveWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/MediaSaveWebhookEvent.cs
index 9a7dcaa3d5..6907067b87 100644
--- a/src/Umbraco.Core/Webhooks/Events/MediaSaveWebhookEvent.cs
+++ b/src/Umbraco.Core/Webhooks/Events/MediaSaveWebhookEvent.cs
@@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Sync;
namespace Umbraco.Cms.Core.Webhooks.Events;
+[WebhookEvent("Media was saved", Constants.WebhookEvents.Types.Media)]
public class MediaSaveWebhookEvent : WebhookEventContentBase
{
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
@@ -26,13 +27,14 @@ public class MediaSaveWebhookEvent : WebhookEventContentBase Constants.WebhookEvents.Aliases.MediaSave;
+
protected override IEnumerable GetEntitiesFromNotification(MediaSavedNotification notification) => notification.SavedEntities;
protected override object? ConvertEntityToRequestPayload(IMedia entity)
diff --git a/src/Umbraco.Core/Webhooks/IWebhookEvent.cs b/src/Umbraco.Core/Webhooks/IWebhookEvent.cs
index 954055d104..693cc22193 100644
--- a/src/Umbraco.Core/Webhooks/IWebhookEvent.cs
+++ b/src/Umbraco.Core/Webhooks/IWebhookEvent.cs
@@ -3,4 +3,8 @@
public interface IWebhookEvent
{
string EventName { get; }
+
+ string EventType { get; }
+
+ string Alias { get; }
}
diff --git a/src/Umbraco.Core/Webhooks/WebhookEventAttribute.cs b/src/Umbraco.Core/Webhooks/WebhookEventAttribute.cs
new file mode 100644
index 0000000000..3c9015e8bf
--- /dev/null
+++ b/src/Umbraco.Core/Webhooks/WebhookEventAttribute.cs
@@ -0,0 +1,26 @@
+namespace Umbraco.Cms.Core.Webhooks;
+
+[AttributeUsage(AttributeTargets.Class)]
+public class WebhookEventAttribute : Attribute
+{
+ public WebhookEventAttribute(string name)
+ : this(name, Constants.WebhookEvents.Types.Other)
+ {
+ }
+
+ public WebhookEventAttribute(string name, string eventType)
+ {
+ Name = name;
+ EventType = eventType;
+ }
+
+ ///
+ /// Gets the friendly name of the event.
+ ///
+ public string? Name { get; }
+
+ ///
+ /// Gets the type of event.
+ ///
+ public string? EventType { get; }
+}
diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs
index 8753eeecf9..cd1666135a 100644
--- a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs
+++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs
@@ -1,4 +1,4 @@
-using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
@@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
+using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Webhooks;
@@ -14,26 +15,37 @@ public abstract class WebhookEventBase : IWebhookEvent, INotifica
{
private readonly IServerRoleAccessor _serverRoleAccessor;
- ///
+ public abstract string Alias { get; }
+
public string EventName { get; set; }
+ public string EventType { get; }
+
protected IWebhookFiringService WebhookFiringService { get; }
+
protected IWebHookService WebHookService { get; }
+
protected WebhookSettings WebhookSettings { get; private set; }
+
+
protected WebhookEventBase(
IWebhookFiringService webhookFiringService,
IWebHookService webHookService,
IOptionsMonitor webhookSettings,
- IServerRoleAccessor serverRoleAccessor,
- string eventName)
+ IServerRoleAccessor serverRoleAccessor)
{
- EventName = eventName;
WebhookFiringService = webhookFiringService;
WebHookService = webHookService;
_serverRoleAccessor = serverRoleAccessor;
+ // assign properties based on the attribute, if it is found
+ WebhookEventAttribute? attribute = GetType().GetCustomAttribute(false);
+
+ EventType = attribute?.EventType ?? "Others";
+ EventName = attribute?.Name ?? Alias;
+
WebhookSettings = webhookSettings.CurrentValue;
webhookSettings.OnChange(x => WebhookSettings = x);
}
@@ -50,7 +62,7 @@ public abstract class WebhookEventBase : IWebhookEvent, INotifica
continue;
}
- await WebhookFiringService.FireAsync(webhook, EventName, notification, cancellationToken);
+ await WebhookFiringService.FireAsync(webhook, Alias, notification, cancellationToken);
}
}
@@ -79,7 +91,7 @@ public abstract class WebhookEventBase : IWebhookEvent, INotifica
return;
}
- IEnumerable webhooks = await WebHookService.GetByEventNameAsync(EventName);
+ IEnumerable webhooks = await WebHookService.GetByAliasAsync(Alias);
await ProcessWebhooks(notification, webhooks, cancellationToken);
}
diff --git a/src/Umbraco.Core/Webhooks/WebhookEventContentBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventContentBase.cs
index 5b8b8c626e..3f7cd4c7b2 100644
--- a/src/Umbraco.Core/Webhooks/WebhookEventContentBase.cs
+++ b/src/Umbraco.Core/Webhooks/WebhookEventContentBase.cs
@@ -16,9 +16,8 @@ public abstract class WebhookEventContentBase : WebhookE
IWebhookFiringService webhookFiringService,
IWebHookService webHookService,
IOptionsMonitor webhookSettings,
- IServerRoleAccessor serverRoleAccessor,
- string eventName)
- : base(webhookFiringService, webHookService, webhookSettings, serverRoleAccessor, eventName)
+ IServerRoleAccessor serverRoleAccessor)
+ : base(webhookFiringService, webHookService, webhookSettings, serverRoleAccessor)
{
}
@@ -38,7 +37,7 @@ public abstract class WebhookEventContentBase : WebhookE
continue;
}
- await WebhookFiringService.FireAsync(webhook, EventName, ConvertEntityToRequestPayload(entity), cancellationToken);
+ await WebhookFiringService.FireAsync(webhook, Alias, ConvertEntityToRequestPayload(entity), cancellationToken);
}
}
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
index 306f7869f7..3a4e715228 100644
--- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs
@@ -97,5 +97,6 @@ public class UmbracoPlan : MigrationPlan
// To 13.0.0
To("{C76D9C9A-635B-4D2C-A301-05642A523E9D}");
+ To("{D5139400-E507-4259-A542-C67358F7E329}");
}
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/RenameEventNameColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/RenameEventNameColumn.cs
new file mode 100644
index 0000000000..7a69a2441f
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_13_0_0/RenameEventNameColumn.cs
@@ -0,0 +1,28 @@
+using Umbraco.Cms.Core;
+
+namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_13_0_0;
+
+public class RenameEventNameColumn : MigrationBase
+{
+ public RenameEventNameColumn(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, "eventName") is false)
+ {
+ return;
+ }
+
+ Rename
+ .Column("eventName")
+ .OnTable(Constants.DatabaseSchema.Tables.WebhookLog)
+ .To("eventAlias")
+ .Do();
+ }
+
+}
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs
index a8606c7391..a9588ecb7d 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs
@@ -33,9 +33,9 @@ internal class WebhookLogDto
[NullSetting(NullSetting = NullSettings.NotNull)]
public string Url { get; set; } = string.Empty;
- [Column(Name = "eventName")]
+ [Column(Name = "eventAlias")]
[NullSetting(NullSetting = NullSettings.NotNull)]
- public string EventName { get; set; } = string.Empty;
+ public string EventAlias { get; set; } = string.Empty;
[Column(Name = "retryCount")]
[NullSetting(NullSetting = NullSettings.NotNull)]
diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs
index 2cc6d5d55b..ff1378ed2d 100644
--- a/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Factories/WebhookLogFactory.cs
@@ -10,7 +10,7 @@ internal static class WebhookLogFactory
new()
{
Date = log.Date,
- EventName = log.EventName,
+ EventAlias = log.EventAlias,
RequestBody = log.RequestBody ?? string.Empty,
ResponseBody = log.ResponseBody,
RetryCount = log.RetryCount,
@@ -27,7 +27,7 @@ internal static class WebhookLogFactory
new()
{
Date = dto.Date,
- EventName = dto.EventName,
+ EventAlias = dto.EventAlias,
RequestBody = dto.RequestBody,
ResponseBody = dto.ResponseBody,
RetryCount = dto.RetryCount,
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs
index af6d651e44..b8fe5e52fe 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/WebhookRepository.cs
@@ -57,14 +57,14 @@ public class WebhookRepository : IWebhookRepository
return webhookDto is null ? null : await DtoToEntity(webhookDto);
}
- public async Task> GetByEventNameAsync(string eventName)
+ public async Task> GetByAliasAsync(string alias)
{
Sql? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql()
.SelectAll()
.From()
.InnerJoin()
.On(left => left.Id, right => right.WebhookId)
- .Where(x => x.Event == eventName);
+ .Where(x => x.Event == alias);
List? webhookDtos = await _scopeAccessor.AmbientScope?.Database.FetchAsync(sql)!;
diff --git a/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs b/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs
index cf09c0d3a2..67b55c913e 100644
--- a/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs
+++ b/src/Umbraco.Infrastructure/Services/Implement/WebhookFiringService.cs
@@ -29,11 +29,11 @@ public class WebhookFiringService : IWebhookFiringService
// TODO: Add queing instead of processing directly in thread
// as this just makes save and publish longer
- public async Task FireAsync(Webhook webhook, string eventName, object? payload, CancellationToken cancellationToken)
+ 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, eventName, payload, retry, cancellationToken);
+ HttpResponseMessage response = await SendRequestAsync(webhook, eventAlias, payload, retry, cancellationToken);
if (response.IsSuccessStatusCode)
{
@@ -42,13 +42,13 @@ public class WebhookFiringService : IWebhookFiringService
}
}
- private async Task SendRequestAsync(Webhook webhook, string eventName, object? payload, int retryCount, CancellationToken cancellationToken)
+ 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", eventName);
+ stringContent.Headers.TryAddWithoutValidation("Umb-Webhook-Event", eventAlias);
foreach (KeyValuePair header in webhook.Headers)
{
@@ -64,7 +64,7 @@ public class WebhookFiringService : IWebhookFiringService
};
- WebhookLog log = await _webhookLogFactory.CreateAsync(eventName, webhookResponseModel, webhook, cancellationToken);
+ WebhookLog log = await _webhookLogFactory.CreateAsync(eventAlias, webhookResponseModel, webhook, cancellationToken);
await _webhookLogService.CreateAsync(log);
return response;
diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs
index 9be5372ce5..49b80144a7 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs
@@ -4,6 +4,7 @@ using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Webhooks;
+using Umbraco.Cms.Web.BackOffice.Services;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Models;
@@ -16,13 +17,15 @@ public class WebhookController : UmbracoAuthorizedJsonController
private readonly IUmbracoMapper _umbracoMapper;
private readonly WebhookEventCollection _webhookEventCollection;
private readonly IWebhookLogService _webhookLogService;
+ private readonly IWebhookPresentationFactory _webhookPresentationFactory;
- public WebhookController(IWebHookService webHookService, IUmbracoMapper umbracoMapper, WebhookEventCollection webhookEventCollection, IWebhookLogService webhookLogService)
+ public WebhookController(IWebHookService webHookService, IUmbracoMapper umbracoMapper, WebhookEventCollection webhookEventCollection, IWebhookLogService webhookLogService, IWebhookPresentationFactory webhookPresentationFactory)
{
_webHookService = webHookService;
_umbracoMapper = umbracoMapper;
_webhookEventCollection = webhookEventCollection;
_webhookLogService = webhookLogService;
+ _webhookPresentationFactory = webhookPresentationFactory;
}
[HttpGet]
@@ -30,7 +33,7 @@ public class WebhookController : UmbracoAuthorizedJsonController
{
PagedModel webhooks = await _webHookService.GetAllAsync(skip, take);
- List webhookViewModels = _umbracoMapper.MapEnumerable(webhooks.Items);
+ IEnumerable webhookViewModels = webhooks.Items.Select(_webhookPresentationFactory.Create);
return Ok(webhookViewModels);
}
diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs
index c973963495..dfa22b06e8 100644
--- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -120,6 +120,7 @@ public static partial class UmbracoBuilderExtensions
builder.Services.AddUnique();
builder.Services.AddSingleton();
builder.Services.AddTransient();
+ builder.Services.AddUnique();
return builder;
}
diff --git a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs
index c797ce67ee..8d9982d0c6 100644
--- a/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs
+++ b/src/Umbraco.Web.BackOffice/Mapping/WebhookMapDefinition.cs
@@ -10,7 +10,6 @@ public class WebhookMapDefinition : IMapDefinition
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);
}
@@ -19,7 +18,7 @@ public class WebhookMapDefinition : IMapDefinition
private void Map(WebhookViewModel source, Webhook target, MapperContext context)
{
target.ContentTypeKeys = source.ContentTypeKeys;
- target.Events = source.Events;
+ target.Events = source.Events.Select(x => x.Alias).ToArray();
target.Url = source.Url;
target.Enabled = source.Enabled;
target.Key = source.Key ?? Guid.NewGuid();
@@ -27,24 +26,18 @@ public class WebhookMapDefinition : IMapDefinition
}
// Umbraco.Code.MapAll
- private void Map(Webhook source, WebhookViewModel target, MapperContext context)
+ private void Map(IWebhookEvent source, WebhookEventViewModel target, MapperContext context)
{
- target.ContentTypeKeys = source.ContentTypeKeys;
- target.Events = source.Events;
- target.Url = source.Url;
- target.Enabled = source.Enabled;
- target.Key = source.Key;
- target.Headers = source.Headers;
+ target.EventName = source.EventName;
+ target.EventType = source.EventType;
+ target.Alias = source.Alias;
}
- // 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.EventAlias = source.EventAlias;
target.Key = source.Key;
target.RequestBody = source.RequestBody ?? string.Empty;
target.ResponseBody = source.ResponseBody;
diff --git a/src/Umbraco.Web.BackOffice/Services/IWebhookPresentationFactory.cs b/src/Umbraco.Web.BackOffice/Services/IWebhookPresentationFactory.cs
new file mode 100644
index 0000000000..5d94607998
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Services/IWebhookPresentationFactory.cs
@@ -0,0 +1,10 @@
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Web.Common.Models;
+
+namespace Umbraco.Cms.Web.BackOffice.Services;
+
+[Obsolete("Will be moved to a new namespace in V14")]
+public interface IWebhookPresentationFactory
+{
+ WebhookViewModel Create(Webhook webhook);
+}
diff --git a/src/Umbraco.Web.BackOffice/Services/WebhookPresentationFactory.cs b/src/Umbraco.Web.BackOffice/Services/WebhookPresentationFactory.cs
new file mode 100644
index 0000000000..57ef7e5f2a
--- /dev/null
+++ b/src/Umbraco.Web.BackOffice/Services/WebhookPresentationFactory.cs
@@ -0,0 +1,37 @@
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Webhooks;
+using Umbraco.Cms.Web.Common.Models;
+
+namespace Umbraco.Cms.Web.BackOffice.Services;
+
+internal class WebhookPresentationFactory : IWebhookPresentationFactory
+{
+ private readonly WebhookEventCollection _webhookEventCollection;
+
+ public WebhookPresentationFactory(WebhookEventCollection webhookEventCollection) => _webhookEventCollection = webhookEventCollection;
+
+ public WebhookViewModel Create(Webhook webhook)
+ {
+ var target = new WebhookViewModel
+ {
+ ContentTypeKeys = webhook.ContentTypeKeys, Events = webhook.Events.Select(Create).ToArray(), Url = webhook.Url,
+ Enabled = webhook.Enabled,
+ Key = webhook.Key,
+ Headers = webhook.Headers,
+ };
+
+ return target;
+ }
+
+ private WebhookEventViewModel Create(string alias)
+ {
+ IWebhookEvent? webhookEvent = _webhookEventCollection.FirstOrDefault(x => x.Alias == alias);
+ return new WebhookEventViewModel
+ {
+ EventName = webhookEvent?.EventName ?? alias,
+ EventType = webhookEvent?.EventType ?? Constants.WebhookEvents.Types.Other,
+ Alias = alias,
+ };
+ }
+}
diff --git a/src/Umbraco.Web.Common/Models/WebhookEventViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookEventViewModel.cs
index 441a367429..05243d4eb6 100644
--- a/src/Umbraco.Web.Common/Models/WebhookEventViewModel.cs
+++ b/src/Umbraco.Web.Common/Models/WebhookEventViewModel.cs
@@ -1,4 +1,5 @@
using System.Runtime.Serialization;
+using Umbraco.Cms.Core.Webhooks;
namespace Umbraco.Cms.Web.Common.Models;
@@ -7,4 +8,10 @@ public class WebhookEventViewModel
{
[DataMember(Name = "eventName")]
public string EventName { get; set; } = string.Empty;
+
+ [DataMember(Name = "eventType")]
+ public string EventType { get; set; } = string.Empty;
+
+ [DataMember(Name = "alias")]
+ public string Alias { get; set; } = string.Empty;
}
diff --git a/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs
index f9bf6762f8..f63282b7cb 100644
--- a/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs
+++ b/src/Umbraco.Web.Common/Models/WebhookLogViewModel.cs
@@ -17,8 +17,8 @@ public class WebhookLogViewModel
[DataMember(Name = "date")]
public DateTime Date { get; set; }
- [DataMember(Name = "eventName")]
- public string EventName { get; set; } = string.Empty;
+ [DataMember(Name = "eventAlias")]
+ public string EventAlias { get; set; } = string.Empty;
[DataMember(Name = "url")]
public string Url { get; set; } = string.Empty;
diff --git a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs
index a0efff398b..9030ff050c 100644
--- a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs
+++ b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs
@@ -12,7 +12,7 @@ public class WebhookViewModel
public string Url { get; set; } = string.Empty;
[DataMember(Name = "events")]
- public string[] Events { get; set; } = Array.Empty();
+ public WebhookEventViewModel[] Events { get; set; } = Array.Empty();
[DataMember(Name = "contentTypeKeys")]
public Guid[] ContentTypeKeys { get; set; } = Array.Empty();
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 67bea3c07b..2e19a102a2 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
@@ -1,7 +1,7 @@
(function () {
"use strict";
- function LanguagePickerController($scope, languageResource, localizationService, webhooksResource) {
+ function EventPickerController($scope, languageResource, localizationService, webhooksResource) {
var vm = this;
@@ -38,10 +38,10 @@
.then((data) => {
let selectedEvents = [];
data.forEach(function (event) {
- let eventObject = { name: event.eventName, selected: false}
- vm.events.push(eventObject);
- if($scope.model.selectedEvents && $scope.model.selectedEvents.includes(eventObject.name)){
- selectedEvents.push(eventObject);
+ event.selected = false;
+ vm.events.push(event);
+ if($scope.model.selectedEvents && $scope.model.selectedEvents.some(x => x.alias === event.alias)){
+ selectedEvents.push(event);
}
});
@@ -55,19 +55,15 @@
if (!event.selected) {
event.selected = true;
$scope.model.selection.push(event);
+
// Only filter if we have not selected an item yet.
if($scope.model.selection.length === 1){
- if(event.name.toLowerCase().includes("content")){
- vm.events = vm.events.filter(event => event.name.toLowerCase().includes("content"));
- }
- else if (event.name.toLowerCase().includes("media")){
- vm.events = vm.events.filter(event => event.name.toLowerCase().includes("media"));
- }
+ vm.events = vm.events.filter(x => x.eventType === event.eventType);
}
- } else {
-
+ }
+ else {
$scope.model.selection.forEach(function (selectedEvent, index) {
- if (selectedEvent.name === event.name) {
+ if (selectedEvent.alias === event.alias) {
event.selected = false;
$scope.model.selection.splice(index, 1);
}
@@ -75,6 +71,7 @@
if($scope.model.selection.length === 0){
vm.events = [];
+ $scope.model.selectedEvents = [];
getAllEvents();
}
}
@@ -82,7 +79,6 @@
function submit(model) {
if ($scope.model.submit) {
- $scope.model.selection = $scope.model.selection.map((item) => item.name)
$scope.model.submit(model);
}
}
@@ -97,6 +93,6 @@
}
- angular.module("umbraco").controller("Umbraco.Editors.EventPickerController", LanguagePickerController);
+ angular.module("umbraco").controller("Umbraco.Editors.EventPickerController", EventPickerController);
})();
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 d4033784ed..e65a5f767f 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
@@ -20,7 +20,7 @@
- {{ event.name }}
+ {{ event.eventName }}
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 6c13daf764..f2db3096af 100644
--- a/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.html
+++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/logs.html
@@ -22,7 +22,7 @@
| {{ log.webhookKey }} |
{{ log.date }} |
{{ log.url }} |
- {{ log.eventName }} |
+ {{ log.eventAlias }} |
{{ log.retryCount }} |
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 3878c1ae87..d66c1463e7 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
@@ -22,7 +22,7 @@
Event
-
{{model.log.eventName}}
+
{{model.log.eventAlias}}
Retry count
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 9df465f738..6f8a17ceb8 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,7 +1,7 @@
-(function () {
+(function () {
"use strict";
- function EditController($scope, editorService, contentTypeResource, mediaTypeResource) {
+ function EditController($scope, editorService, contentTypeResource, mediaTypeResource, memberTypeResource) {
const vm = this;
@@ -29,7 +29,7 @@
}
function openContentTypePicker() {
- const isContent = $scope.model.webhook ? $scope.model.webhook.events[0].toLowerCase().includes("content") : null;
+ const eventType = $scope.model.webhook ? $scope.model.webhook.events[0].eventType.toLowerCase() : null;
const editor = {
multiPicker: true,
@@ -39,7 +39,7 @@
return item.nodeType === "container"; // || item.metaData.isElement || !!_.findWhere(vm.itemTypes, { udi: item.udi });
},
submit(model) {
- getEntities(model.selection, isContent);
+ getEntities(model.selection, eventType);
$scope.model.webhook.contentTypeKeys = model.selection.map(item => item.key);
editorService.close();
},
@@ -48,9 +48,7 @@
}
};
- const itemType = isContent ? "content" : "media";
-
- switch (itemType) {
+ switch (eventType.toLowerCase()) {
case "content":
editorService.contentTypePicker(editor);
break;
@@ -82,8 +80,22 @@
});
}
- function getEntities(selection, isContent) {
- const resource = isContent ? contentTypeResource : mediaTypeResource;
+ function getEntities(selection, eventType) {
+ let resource;
+ switch (eventType.toCamelCase()) {
+ case "content":
+ resource = contentTypeResource;
+ break;
+ case "media":
+ resource = mediaTypeResource;
+ break;
+ case "member":
+ resource = memberTypeResource;
+ break;
+ default:
+ return;
+ }
+
$scope.model.contentTypes = [];
selection.forEach(entity => {
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 cf55320f2f..4bc61fec98 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
@@ -42,9 +42,9 @@
{
- vm.events = data.map(item => item.eventName);
+ vm.events = data;
});
}
function resolveEventNames(webhook) {
webhook.events.forEach(event => {
if (!vm.webhookEvents[webhook.key]) {
- vm.webhookEvents[webhook.key] = event;
+ vm.webhookEvents[webhook.key] = event.eventName;
} else {
- vm.webhookEvents[webhook.key] += ", " + event;
+ vm.webhookEvents[webhook.key] += ", " + event.eventName;
}
});
}
+ function determineResource(resourceType){
+ let resource;
+ switch (resourceType) {
+ case "content":
+ resource = contentTypeResource;
+ break;
+ case "media":
+ resource = mediaTypeResource;
+ break;
+ case "member":
+ resource = memberTypeResource;
+ break;
+ default:
+ return;
+ }
+
+ return resource;
+ }
+
function getEntities(webhook) {
- const isContent = webhook.events[0].toLowerCase().includes("content");
- const resource = isContent ? contentTypeResource : mediaTypeResource;
+ let resource = determineResource(webhook.events[0].eventType.toLowerCase());
let entities = [];
webhook.contentTypeKeys.forEach(key => {
@@ -68,8 +86,7 @@
}
function resolveTypeNames(webhook) {
- const isContent = webhook.events[0].toLowerCase().includes("content");
- const resource = isContent ? contentTypeResource : mediaTypeResource;
+ let resource = determineResource(webhook.events[0].eventType.toLowerCase());
if (vm.webHooksContentTypes[webhook.key]){
delete vm.webHooksContentTypes[webhook.key];
@@ -154,7 +171,7 @@
}
function loadWebhooks(){
- webhooksResource
+ return webhooksResource
.getAll()
.then(result => {
vm.webhooks = result;
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs
index af75becd1c..b4959de8e9 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookLogServiceTests.cs
@@ -21,7 +21,7 @@ public class WebhookLogServiceTests : UmbracoIntegrationTest
var createdWebhookLog = await WebhookLogService.CreateAsync(new WebhookLog
{
Date = DateTime.UtcNow,
- EventName = Constants.WebhookEvents.ContentPublish,
+ EventAlias = Constants.WebhookEvents.Aliases.ContentPublish,
RequestBody = "Test Request Body",
ResponseBody = "Test response body",
StatusCode = "200",
@@ -39,7 +39,7 @@ public class WebhookLogServiceTests : UmbracoIntegrationTest
Assert.AreEqual(1, webhookLogsPaged.Items.Count());
var webHookLog = webhookLogsPaged.Items.First();
Assert.AreEqual(createdWebhookLog.Date.ToString(CultureInfo.InvariantCulture), webHookLog.Date.ToString(CultureInfo.InvariantCulture));
- Assert.AreEqual(createdWebhookLog.EventName, webHookLog.EventName);
+ Assert.AreEqual(createdWebhookLog.EventAlias, webHookLog.EventAlias);
Assert.AreEqual(createdWebhookLog.RequestBody, webHookLog.RequestBody);
Assert.AreEqual(createdWebhookLog.ResponseBody, webHookLog.ResponseBody);
Assert.AreEqual(createdWebhookLog.StatusCode, webHookLog.StatusCode);
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs
index 6f6da74485..609388dfaf 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/WebhookServiceTests.cs
@@ -14,11 +14,11 @@ public class WebhookServiceTests : UmbracoIntegrationTest
private IWebHookService WebhookService => GetRequiredService();
[Test]
- [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")]
+ [TestCase("https://example.com", Constants.WebhookEvents.Aliases.ContentPublish, "00000000-0000-0000-0000-010000000000")]
+ [TestCase("https://example.com", Constants.WebhookEvents.Aliases.ContentDelete, "00000000-0000-0000-0000-000200000000")]
+ [TestCase("https://example.com", Constants.WebhookEvents.Aliases.ContentUnpublish, "00000000-0000-0000-0000-300000000000")]
+ [TestCase("https://example.com", Constants.WebhookEvents.Aliases.MediaDelete, "00000000-0000-0000-0000-000004000000")]
+ [TestCase("https://example.com", Constants.WebhookEvents.Aliases.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 }));
@@ -37,9 +37,9 @@ public class WebhookServiceTests : UmbracoIntegrationTest
[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 createdWebhookOne = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.Aliases.ContentPublish }));
+ var createdWebhookTwo = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.Aliases.ContentDelete }));
+ var createdWebhookThree = await WebhookService.CreateAsync(new Webhook("https://example.com", true, new[] { Guid.NewGuid() }, new[] { Constants.WebhookEvents.Aliases.ContentUnpublish }));
var webhooks = await WebhookService.GetAllAsync(0, int.MaxValue);
Assert.Multiple(() =>
@@ -52,11 +52,11 @@ public class WebhookServiceTests : UmbracoIntegrationTest
}
[Test]
- [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")]
+ [TestCase("https://example.com", Constants.WebhookEvents.Aliases.ContentPublish, "00000000-0000-0000-0000-010000000000")]
+ [TestCase("https://example.com", Constants.WebhookEvents.Aliases.ContentDelete, "00000000-0000-0000-0000-000200000000")]
+ [TestCase("https://example.com", Constants.WebhookEvents.Aliases.ContentUnpublish, "00000000-0000-0000-0000-300000000000")]
+ [TestCase("https://example.com", Constants.WebhookEvents.Aliases.MediaDelete, "00000000-0000-0000-0000-000004000000")]
+ [TestCase("https://example.com", Constants.WebhookEvents.Aliases.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 }));
@@ -71,7 +71,7 @@ public class WebhookServiceTests : UmbracoIntegrationTest
[Test]
public async Task Can_Create_With_No_EntityKeys()
{
- var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.ContentPublish }));
+ var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.Aliases.ContentPublish }));
var webhook = await WebhookService.GetAsync(createdWebhook.Key);
Assert.IsNotNull(webhook);
@@ -81,24 +81,24 @@ public class WebhookServiceTests : UmbracoIntegrationTest
[Test]
public async Task Can_Update()
{
- var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.ContentPublish }));
- createdWebhook.Events = new[] { Constants.WebhookEvents.ContentDelete };
+ var createdWebhook = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.Aliases.ContentPublish }));
+ createdWebhook.Events = new[] { Constants.WebhookEvents.Aliases.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(Constants.WebhookEvents.ContentDelete));
+ Assert.IsTrue(updatedWebhook.Events.Contains(Constants.WebhookEvents.Aliases.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 webhook1 = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.Aliases.ContentPublish }));
+ var webhook2 = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.Aliases.ContentUnpublish }));
+ var webhook3 = await WebhookService.CreateAsync(new Webhook("https://example.com", events: new[] { Constants.WebhookEvents.Aliases.ContentUnpublish }));
- var result = await WebhookService.GetByEventNameAsync(Constants.WebhookEvents.ContentUnpublish);
+ var result = await WebhookService.GetByAliasAsync(Constants.WebhookEvents.Aliases.ContentUnpublish);
Assert.IsNotEmpty(result);
Assert.AreEqual(2, result.Count());
From e143133bcfb955fd593314a28a405c042f4ed62b Mon Sep 17 00:00:00 2001
From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
Date: Fri, 10 Nov 2023 08:20:22 +0100
Subject: [PATCH 09/36] V13: Update IWebHookService to proper casing (#15169)
* Update to proper casing
* Fixed naming
---------
Co-authored-by: Zeegaan
Co-authored-by: Andreas Zerbst
---
.../DependencyInjection/UmbracoBuilder.cs | 2 +-
.../{IWebHookService.cs => IWebhookService.cs} | 2 +-
src/Umbraco.Core/Services/WebhookService.cs | 2 +-
.../Webhooks/Events/ContentDeleteWebhookEvent.cs | 4 ++--
.../Events/ContentPublishWebhookEvent.cs | 4 ++--
.../Events/ContentUnpublishWebhookEvent.cs | 4 ++--
.../Webhooks/Events/MediaDeleteWebhookEvent.cs | 4 ++--
.../Webhooks/Events/MediaSaveWebhookEvent.cs | 4 ++--
src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 8 ++++----
.../Webhooks/WebhookEventContentBase.cs | 4 ++--
.../Controllers/WebhookController.cs | 16 ++++++++--------
.../src/views/webhooks/webhooks.controller.js | 14 +++++++-------
.../src/views/webhooks/webhooks.html | 2 +-
.../Services/WebhookLogServiceTests.cs | 16 ++++++++--------
.../Umbraco.Core/Services/WebhookServiceTests.cs | 2 +-
15 files changed, 44 insertions(+), 44 deletions(-)
rename src/Umbraco.Core/Services/{IWebHookService.cs => IWebhookService.cs} (96%)
diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
index 0e132d3fed..354ef7a40c 100644
--- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
+++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs
@@ -332,7 +332,7 @@ namespace Umbraco.Cms.Core.DependencyInjection
// Register filestream security analyzers
Services.AddUnique();
Services.AddUnique();
- Services.AddUnique();
+ Services.AddUnique();
Services.AddUnique();
Services.AddUnique();
}
diff --git a/src/Umbraco.Core/Services/IWebHookService.cs b/src/Umbraco.Core/Services/IWebhookService.cs
similarity index 96%
rename from src/Umbraco.Core/Services/IWebHookService.cs
rename to src/Umbraco.Core/Services/IWebhookService.cs
index 785f5e8701..d38adaa999 100644
--- a/src/Umbraco.Core/Services/IWebHookService.cs
+++ b/src/Umbraco.Core/Services/IWebhookService.cs
@@ -2,7 +2,7 @@ using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Services;
-public interface IWebHookService
+public interface IWebhookService
{
///
/// Creates a webhook.
diff --git a/src/Umbraco.Core/Services/WebhookService.cs b/src/Umbraco.Core/Services/WebhookService.cs
index 424f1afb14..e6d5f405b4 100644
--- a/src/Umbraco.Core/Services/WebhookService.cs
+++ b/src/Umbraco.Core/Services/WebhookService.cs
@@ -4,7 +4,7 @@ using Umbraco.Cms.Core.Scoping;
namespace Umbraco.Cms.Core.Services;
-public class WebhookService : IWebHookService
+public class WebhookService : IWebhookService
{
private readonly ICoreScopeProvider _provider;
private readonly IWebhookRepository _webhookRepository;
diff --git a/src/Umbraco.Core/Webhooks/Events/ContentDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/ContentDeleteWebhookEvent.cs
index d8386b9914..85a1f39ba9 100644
--- a/src/Umbraco.Core/Webhooks/Events/ContentDeleteWebhookEvent.cs
+++ b/src/Umbraco.Core/Webhooks/Events/ContentDeleteWebhookEvent.cs
@@ -12,12 +12,12 @@ public class ContentDeleteWebhookEvent : WebhookEventContentBase webhookSettings,
IServerRoleAccessor serverRoleAccessor)
: base(
webhookFiringService,
- webHookService,
+ webhookService,
webhookSettings,
serverRoleAccessor)
{
diff --git a/src/Umbraco.Core/Webhooks/Events/ContentPublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/ContentPublishWebhookEvent.cs
index 03f2e71706..e1ba7125ec 100644
--- a/src/Umbraco.Core/Webhooks/Events/ContentPublishWebhookEvent.cs
+++ b/src/Umbraco.Core/Webhooks/Events/ContentPublishWebhookEvent.cs
@@ -18,14 +18,14 @@ public class ContentPublishWebhookEvent : WebhookEventContentBase webhookSettings,
IServerRoleAccessor serverRoleAccessor,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IApiContentBuilder apiContentBuilder)
: base(
webhookFiringService,
- webHookService,
+ webhookService,
webhookSettings,
serverRoleAccessor)
{
diff --git a/src/Umbraco.Core/Webhooks/Events/ContentUnpublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/ContentUnpublishWebhookEvent.cs
index 354cdb295b..76499eb277 100644
--- a/src/Umbraco.Core/Webhooks/Events/ContentUnpublishWebhookEvent.cs
+++ b/src/Umbraco.Core/Webhooks/Events/ContentUnpublishWebhookEvent.cs
@@ -12,12 +12,12 @@ public class ContentUnpublishWebhookEvent : WebhookEventContentBase webhookSettings,
IServerRoleAccessor serverRoleAccessor)
: base(
webhookFiringService,
- webHookService,
+ webhookService,
webhookSettings,
serverRoleAccessor)
{
diff --git a/src/Umbraco.Core/Webhooks/Events/MediaDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/MediaDeleteWebhookEvent.cs
index ab0fd98942..ba9fb2333a 100644
--- a/src/Umbraco.Core/Webhooks/Events/MediaDeleteWebhookEvent.cs
+++ b/src/Umbraco.Core/Webhooks/Events/MediaDeleteWebhookEvent.cs
@@ -12,12 +12,12 @@ public class MediaDeleteWebhookEvent : WebhookEventContentBase webhookSettings,
IServerRoleAccessor serverRoleAccessor)
: base(
webhookFiringService,
- webHookService,
+ webhookService,
webhookSettings,
serverRoleAccessor)
{
diff --git a/src/Umbraco.Core/Webhooks/Events/MediaSaveWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/MediaSaveWebhookEvent.cs
index 6907067b87..cea7181d51 100644
--- a/src/Umbraco.Core/Webhooks/Events/MediaSaveWebhookEvent.cs
+++ b/src/Umbraco.Core/Webhooks/Events/MediaSaveWebhookEvent.cs
@@ -18,14 +18,14 @@ public class MediaSaveWebhookEvent : WebhookEventContentBase webhookSettings,
IServerRoleAccessor serverRoleAccessor,
IPublishedSnapshotAccessor publishedSnapshotAccessor,
IApiMediaBuilder apiMediaBuilder)
: base(
webhookFiringService,
- webHookService,
+ webhookService,
webhookSettings,
serverRoleAccessor)
{
diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs
index cd1666135a..de15e23a00 100644
--- a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs
+++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs
@@ -23,7 +23,7 @@ public abstract class WebhookEventBase : IWebhookEvent, INotifica
protected IWebhookFiringService WebhookFiringService { get; }
- protected IWebHookService WebHookService { get; }
+ protected IWebhookService WebhookService { get; }
protected WebhookSettings WebhookSettings { get; private set; }
@@ -31,13 +31,13 @@ public abstract class WebhookEventBase : IWebhookEvent, INotifica
protected WebhookEventBase(
IWebhookFiringService webhookFiringService,
- IWebHookService webHookService,
+ IWebhookService webhookService,
IOptionsMonitor webhookSettings,
IServerRoleAccessor serverRoleAccessor)
{
WebhookFiringService = webhookFiringService;
- WebHookService = webHookService;
+ WebhookService = webhookService;
_serverRoleAccessor = serverRoleAccessor;
// assign properties based on the attribute, if it is found
@@ -91,7 +91,7 @@ public abstract class WebhookEventBase : IWebhookEvent, INotifica
return;
}
- IEnumerable webhooks = await WebHookService.GetByAliasAsync(Alias);
+ IEnumerable webhooks = await WebhookService.GetByAliasAsync(Alias);
await ProcessWebhooks(notification, webhooks, cancellationToken);
}
diff --git a/src/Umbraco.Core/Webhooks/WebhookEventContentBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventContentBase.cs
index 3f7cd4c7b2..6b72bbaacb 100644
--- a/src/Umbraco.Core/Webhooks/WebhookEventContentBase.cs
+++ b/src/Umbraco.Core/Webhooks/WebhookEventContentBase.cs
@@ -14,10 +14,10 @@ public abstract class WebhookEventContentBase : WebhookE
{
protected WebhookEventContentBase(
IWebhookFiringService webhookFiringService,
- IWebHookService webHookService,
+ IWebhookService webhookService,
IOptionsMonitor webhookSettings,
IServerRoleAccessor serverRoleAccessor)
- : base(webhookFiringService, webHookService, webhookSettings, serverRoleAccessor)
+ : base(webhookFiringService, webhookService, webhookSettings, serverRoleAccessor)
{
}
diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs
index 49b80144a7..68d668645e 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs
@@ -13,15 +13,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers;
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
public class WebhookController : UmbracoAuthorizedJsonController
{
- private readonly IWebHookService _webHookService;
+ private readonly IWebhookService _webhookService;
private readonly IUmbracoMapper _umbracoMapper;
private readonly WebhookEventCollection _webhookEventCollection;
private readonly IWebhookLogService _webhookLogService;
private readonly IWebhookPresentationFactory _webhookPresentationFactory;
- public WebhookController(IWebHookService webHookService, IUmbracoMapper umbracoMapper, WebhookEventCollection webhookEventCollection, IWebhookLogService webhookLogService, IWebhookPresentationFactory webhookPresentationFactory)
+ public WebhookController(IWebhookService webhookService, IUmbracoMapper umbracoMapper, WebhookEventCollection webhookEventCollection, IWebhookLogService webhookLogService, IWebhookPresentationFactory webhookPresentationFactory)
{
- _webHookService = webHookService;
+ _webhookService = webhookService;
_umbracoMapper = umbracoMapper;
_webhookEventCollection = webhookEventCollection;
_webhookLogService = webhookLogService;
@@ -31,7 +31,7 @@ public class WebhookController : UmbracoAuthorizedJsonController
[HttpGet]
public async Task GetAll(int skip = 0, int take = int.MaxValue)
{
- PagedModel webhooks = await _webHookService.GetAllAsync(skip, take);
+ PagedModel webhooks = await _webhookService.GetAllAsync(skip, take);
IEnumerable webhookViewModels = webhooks.Items.Select(_webhookPresentationFactory.Create);
@@ -43,7 +43,7 @@ public class WebhookController : UmbracoAuthorizedJsonController
{
Webhook updateModel = _umbracoMapper.Map(webhookViewModel)!;
- await _webHookService.UpdateAsync(updateModel);
+ await _webhookService.UpdateAsync(updateModel);
return Ok();
}
@@ -52,7 +52,7 @@ public class WebhookController : UmbracoAuthorizedJsonController
public async Task Create(WebhookViewModel webhookViewModel)
{
Webhook webhook = _umbracoMapper.Map(webhookViewModel)!;
- await _webHookService.CreateAsync(webhook);
+ await _webhookService.CreateAsync(webhook);
return Ok();
}
@@ -60,7 +60,7 @@ public class WebhookController : UmbracoAuthorizedJsonController
[HttpGet]
public async Task GetByKey(Guid key)
{
- Webhook? webhook = await _webHookService.GetAsync(key);
+ Webhook? webhook = await _webhookService.GetAsync(key);
return webhook is null ? NotFound() : Ok(webhook);
}
@@ -68,7 +68,7 @@ public class WebhookController : UmbracoAuthorizedJsonController
[HttpDelete]
public async Task Delete(Guid key)
{
- await _webHookService.DeleteAsync(key);
+ await _webhookService.DeleteAsync(key);
return Ok();
}
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 b840facd59..cf85f0df2c 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
@@ -14,7 +14,7 @@
vm.page = {};
vm.webhooks = [];
vm.events = [];
- vm.webHooksContentTypes = {};
+ vm.webhooksContentTypes = {};
vm.webhookEvents = {};
function init() {
@@ -88,17 +88,17 @@
function resolveTypeNames(webhook) {
let resource = determineResource(webhook.events[0].eventType.toLowerCase());
- if (vm.webHooksContentTypes[webhook.key]){
- delete vm.webHooksContentTypes[webhook.key];
+ if (vm.webhooksContentTypes[webhook.key]){
+ delete vm.webhooksContentTypes[webhook.key];
}
webhook.contentTypeKeys.forEach(key => {
resource.getById(key)
.then(data => {
- if (!vm.webHooksContentTypes[webhook.key]) {
- vm.webHooksContentTypes[webhook.key] = data.name;
+ if (!vm.webhooksContentTypes[webhook.key]) {
+ vm.webhooksContentTypes[webhook.key] = data.name;
} else {
- vm.webHooksContentTypes[webhook.key] += ", " + data.name;
+ vm.webhooksContentTypes[webhook.key] += ", " + data.name;
}
});
});
@@ -176,7 +176,7 @@
.then(result => {
vm.webhooks = result;
vm.webhookEvents = {};
- vm.webHooksContentTypes = {};
+ vm.webhooksContentTypes = {};
vm.webhooks.forEach(webhook => {
resolveTypeNames(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 e6f4e8f53d..9b2c1f7461 100644
--- a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.html
+++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.html
@@ -36,7 +36,7 @@
{{ vm.webhookEvents[webhook.key] }}
{{ webhook.url }} |
- {{ vm.webHooksContentTypes[webhook.key] }} |
+ {{ vm.webhooksContentTypes[webhook.key] }} |
GetRequiredService();
+ private IWebhookService WebhookService => GetRequiredService();
[Test]
[TestCase("https://example.com", Constants.WebhookEvents.Aliases.ContentPublish, "00000000-0000-0000-0000-010000000000")]
From b8f5078135c9d77d8842bf8ca9364f5b6326becf Mon Sep 17 00:00:00 2001
From: Ronald Barendse
Date: Fri, 10 Nov 2023 09:07:10 +0100
Subject: [PATCH 10/36] Improve logging of invalid reference relations (#15160)
* Include automatic relation type aliases from factory
* Remove unnessecary distinct and fix SQL parameter overflow issue
* Fixed assertions and test distinct aliases
* Simplified collection assertions
* Improve logging of invalid reference relations
---
.../Implement/ContentRepositoryBase.cs | 72 ++++++++++---------
1 file changed, 40 insertions(+), 32 deletions(-)
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs
index fde1c6ca05..6df4d66ab5 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs
@@ -1084,50 +1084,58 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
{
// Get all references from our core built in DataEditors/Property Editors
// Along with seeing if developers want to collect additional references from the DataValueReferenceFactories collection
- var trackedRelations = _dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors);
+ ISet references = _dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors);
// First delete all auto-relations for this entity
- var relationTypeAliases = _dataValueReferenceFactories.GetAutomaticRelationTypesAliases(entity.Properties, PropertyEditors).ToArray();
- RelationRepository.DeleteByParent(entity.Id, relationTypeAliases);
+ ISet automaticRelationTypeAliases = _dataValueReferenceFactories.GetAutomaticRelationTypesAliases(entity.Properties, PropertyEditors);
+ RelationRepository.DeleteByParent(entity.Id, automaticRelationTypeAliases.ToArray());
- if (trackedRelations.Count == 0)
+ if (references.Count == 0)
{
+ // No need to add new references/relations
return;
}
- var udiToGuids = trackedRelations.Select(x => x.Udi as GuidUdi).WhereNotNull().ToDictionary(x => (Udi)x, x => x.Guid);
+ // Lookup node IDs for all GUID based UDIs
+ IEnumerable keys = references.Select(x => x.Udi).OfType().Select(x => x.Guid);
+ var keysLookup = Database.FetchByGroups(keys, Constants.Sql.MaxParameterCount, guids =>
+ {
+ return Sql()
+ .Select(x => x.NodeId, x => x.UniqueId)
+ .From()
+ .WhereIn(x => x.UniqueId, guids);
+ }).ToDictionary(x => x.UniqueId, x => x.NodeId);
- // lookup in the DB all INT ids for the GUIDs and chuck into a dictionary
- var keyToIds = Database.FetchByGroups(udiToGuids.Values, Constants.Sql.MaxParameterCount, guids => Sql()
- .Select(x => x.NodeId, x => x.UniqueId)
- .From()
- .WhereIn(x => x.UniqueId, guids))
- .ToDictionary(x => x.UniqueId, x => x.NodeId);
+ // Lookup all relation type IDs
+ var relationTypeLookup = RelationTypeRepository.GetMany(Array.Empty()).ToDictionary(x => x.Alias, x => x.Id);
- var allRelationTypes = RelationTypeRepository.GetMany(Array.Empty()).ToDictionary(x => x.Alias, x => x);
-
- IEnumerable toSave = trackedRelations.Select(rel =>
+ // Get all valid relations
+ var relations = new List(references.Count);
+ foreach (UmbracoEntityReference reference in references)
+ {
+ if (!automaticRelationTypeAliases.Contains(reference.RelationTypeAlias))
{
- if (!allRelationTypes.TryGetValue(rel.RelationTypeAlias, out IRelationType? relationType))
- {
- throw new InvalidOperationException($"The relation type {rel.RelationTypeAlias} does not exist");
- }
-
- if (!udiToGuids.TryGetValue(rel.Udi, out Guid guid))
- {
- return null; // This shouldn't happen!
- }
-
- if (!keyToIds.TryGetValue(guid, out var id))
- {
- return null; // This shouldn't happen!
- }
-
- return new ReadOnlyRelation(entity.Id, id, relationType.Id);
- }).WhereNotNull();
+ // Returning a reference that doesn't use an automatic relation type is an issue that should be fixed in code
+ Logger.LogError("The reference to {Udi} uses a relation type {RelationTypeAlias} that is not an automatic relation type.", reference.Udi, reference.RelationTypeAlias);
+ }
+ else if (!relationTypeLookup.TryGetValue(reference.RelationTypeAlias, out int relationTypeId))
+ {
+ // A non-existent relation type could be caused by an environment issue (e.g. it was manually removed)
+ Logger.LogWarning("The reference to {Udi} uses a relation type {RelationTypeAlias} that does not exist.", reference.Udi, reference.RelationTypeAlias);
+ }
+ else if (reference.Udi is not GuidUdi udi || !keysLookup.TryGetValue(udi.Guid, out var id))
+ {
+ // Relations only support references to items that are stored in the NodeDto table (because of foreign key constraints)
+ Logger.LogInformation("The reference to {Udi} can not be saved as relation, because doesn't have a node ID.", reference.Udi);
+ }
+ else
+ {
+ relations.Add(new ReadOnlyRelation(entity.Id, id, relationTypeId));
+ }
+ }
// Save bulk relations
- RelationRepository.SaveBulk(toSave);
+ RelationRepository.SaveBulk(relations);
}
///
From 8f8302358a1ca0847387460f1b83f52ee28309a3 Mon Sep 17 00:00:00 2001
From: Andy Butland
Date: Fri, 10 Nov 2023 10:17:36 +0100
Subject: [PATCH 11/36] Added notifications to the webhook service (#15174)
* Added webhooks to UdiEntityTypes and UdiGetterExtensions.
* Renamed IWebhookservice and amended signature of CreateAsync to allow returning of null.
Added notifications for webhooks.
* Further renames.
* Fixed failing unit test.
* Removed inadvertently added file.
---
src/Umbraco.Core/Constants-UdiEntityType.cs | 2 +
.../Extensions/UdiGetterExtensions.cs | 15 +++++++
.../WebhookDeletedNotification .cs | 12 ++++++
.../WebhookDeletingNotification.cs | 17 ++++++++
.../Notifications/WebhookSavedNotification.cs | 17 ++++++++
.../WebhookSavingNotification.cs | 17 ++++++++
src/Umbraco.Core/Services/IWebhookService.cs | 2 +-
src/Umbraco.Core/Services/WebhookService.cs | 43 ++++++++++++++++++-
src/Umbraco.Core/UdiParser.cs | 3 +-
src/Umbraco.Core/Webhooks/WebhookEventBase.cs | 2 +-
.../Controllers/WebhookController.cs | 2 +-
11 files changed, 126 insertions(+), 6 deletions(-)
create mode 100644 src/Umbraco.Core/Notifications/WebhookDeletedNotification .cs
create mode 100644 src/Umbraco.Core/Notifications/WebhookDeletingNotification.cs
create mode 100644 src/Umbraco.Core/Notifications/WebhookSavedNotification.cs
create mode 100644 src/Umbraco.Core/Notifications/WebhookSavingNotification.cs
diff --git a/src/Umbraco.Core/Constants-UdiEntityType.cs b/src/Umbraco.Core/Constants-UdiEntityType.cs
index f65c290516..beae3391b1 100644
--- a/src/Umbraco.Core/Constants-UdiEntityType.cs
+++ b/src/Umbraco.Core/Constants-UdiEntityType.cs
@@ -46,6 +46,8 @@ public static partial class Constants
public const string RelationType = "relation-type";
+ public const string Webhook = "webhook";
+
// forms
public const string FormsForm = "forms-form";
public const string FormsPreValue = "forms-prevalue";
diff --git a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs
index 03ed07f2fe..fda84c1013 100644
--- a/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs
+++ b/src/Umbraco.Core/Extensions/UdiGetterExtensions.cs
@@ -358,6 +358,21 @@ public static class UdiGetterExtensions
return new GuidUdi(Constants.UdiEntityType.RelationType, entity.Key).EnsureClosed();
}
+ ///
+ /// Gets the entity identifier of the entity.
+ ///
+ /// The entity.
+ /// The entity identifier of the entity.
+ public static GuidUdi GetUdi(this Webhook entity)
+ {
+ if (entity == null)
+ {
+ throw new ArgumentNullException("entity");
+ }
+
+ return new GuidUdi(Constants.UdiEntityType.Webhook, entity.Key).EnsureClosed();
+ }
+
///
/// Gets the entity identifier of the entity.
///
diff --git a/src/Umbraco.Core/Notifications/WebhookDeletedNotification .cs b/src/Umbraco.Core/Notifications/WebhookDeletedNotification .cs
new file mode 100644
index 0000000000..516d52012c
--- /dev/null
+++ b/src/Umbraco.Core/Notifications/WebhookDeletedNotification .cs
@@ -0,0 +1,12 @@
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Notifications;
+
+public class WebhookDeletedNotification : DeletedNotification
+{
+ public WebhookDeletedNotification(Webhook target, EventMessages messages)
+ : base(target, messages)
+ {
+ }
+}
diff --git a/src/Umbraco.Core/Notifications/WebhookDeletingNotification.cs b/src/Umbraco.Core/Notifications/WebhookDeletingNotification.cs
new file mode 100644
index 0000000000..f703113370
--- /dev/null
+++ b/src/Umbraco.Core/Notifications/WebhookDeletingNotification.cs
@@ -0,0 +1,17 @@
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Notifications;
+
+public class WebhookDeletingNotification : DeletingNotification
+{
+ public WebhookDeletingNotification(Webhook target, EventMessages messages)
+ : base(target, messages)
+ {
+ }
+
+ public WebhookDeletingNotification(IEnumerable target, EventMessages messages)
+ : base(target, messages)
+ {
+ }
+}
diff --git a/src/Umbraco.Core/Notifications/WebhookSavedNotification.cs b/src/Umbraco.Core/Notifications/WebhookSavedNotification.cs
new file mode 100644
index 0000000000..efd4fc3707
--- /dev/null
+++ b/src/Umbraco.Core/Notifications/WebhookSavedNotification.cs
@@ -0,0 +1,17 @@
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Notifications;
+
+public class WebhookSavedNotification : SavedNotification
+{
+ public WebhookSavedNotification(Webhook target, EventMessages messages)
+ : base(target, messages)
+ {
+ }
+
+ public WebhookSavedNotification(IEnumerable target, EventMessages messages)
+ : base(target, messages)
+ {
+ }
+}
diff --git a/src/Umbraco.Core/Notifications/WebhookSavingNotification.cs b/src/Umbraco.Core/Notifications/WebhookSavingNotification.cs
new file mode 100644
index 0000000000..69dee928c8
--- /dev/null
+++ b/src/Umbraco.Core/Notifications/WebhookSavingNotification.cs
@@ -0,0 +1,17 @@
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Core.Models;
+
+namespace Umbraco.Cms.Core.Notifications;
+
+public class WebhookSavingNotification : SavingNotification
+{
+ public WebhookSavingNotification(Webhook target, EventMessages messages)
+ : base(target, messages)
+ {
+ }
+
+ public WebhookSavingNotification(IEnumerable target, EventMessages messages)
+ : base(target, messages)
+ {
+ }
+}
diff --git a/src/Umbraco.Core/Services/IWebhookService.cs b/src/Umbraco.Core/Services/IWebhookService.cs
index d38adaa999..38306839ba 100644
--- a/src/Umbraco.Core/Services/IWebhookService.cs
+++ b/src/Umbraco.Core/Services/IWebhookService.cs
@@ -8,7 +8,7 @@ public interface IWebhookService
/// Creates a webhook.
///
/// to create.
- Task CreateAsync(Webhook webhook);
+ Task CreateAsync(Webhook webhook);
///
/// Updates a webhook.
diff --git a/src/Umbraco.Core/Services/WebhookService.cs b/src/Umbraco.Core/Services/WebhookService.cs
index e6d5f405b4..40a827c51e 100644
--- a/src/Umbraco.Core/Services/WebhookService.cs
+++ b/src/Umbraco.Core/Services/WebhookService.cs
@@ -1,4 +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;
@@ -8,18 +10,33 @@ public class WebhookService : IWebhookService
{
private readonly ICoreScopeProvider _provider;
private readonly IWebhookRepository _webhookRepository;
+ private readonly IEventMessagesFactory _eventMessagesFactory;
- public WebhookService(ICoreScopeProvider provider, IWebhookRepository webhookRepository)
+ public WebhookService(ICoreScopeProvider provider, IWebhookRepository webhookRepository, IEventMessagesFactory eventMessagesFactory)
{
_provider = provider;
_webhookRepository = webhookRepository;
+ _eventMessagesFactory = eventMessagesFactory;
}
///
- public async Task CreateAsync(Webhook webhook)
+ public async Task CreateAsync(Webhook webhook)
{
using ICoreScope scope = _provider.CreateCoreScope();
+
+ EventMessages eventMessages = _eventMessagesFactory.Get();
+ var savingNotification = new WebhookSavingNotification(webhook, eventMessages);
+ if (scope.Notifications.PublishCancelable(savingNotification))
+ {
+ scope.Complete();
+ return null;
+ }
+
Webhook created = await _webhookRepository.CreateAsync(webhook);
+
+ scope.Notifications.Publish(
+ new WebhookSavedNotification(webhook, eventMessages).WithStateFrom(savingNotification));
+
scope.Complete();
return created;
@@ -37,6 +54,14 @@ public class WebhookService : IWebhookService
throw new ArgumentException("Webhook does not exist");
}
+ EventMessages eventMessages = _eventMessagesFactory.Get();
+ var savingNotification = new WebhookSavingNotification(webhook, eventMessages);
+ if (scope.Notifications.PublishCancelable(savingNotification))
+ {
+ scope.Complete();
+ return;
+ }
+
currentWebhook.Enabled = webhook.Enabled;
currentWebhook.ContentTypeKeys = webhook.ContentTypeKeys;
currentWebhook.Events = webhook.Events;
@@ -44,6 +69,10 @@ public class WebhookService : IWebhookService
currentWebhook.Headers = webhook.Headers;
await _webhookRepository.UpdateAsync(currentWebhook);
+
+ scope.Notifications.Publish(
+ new WebhookSavedNotification(webhook, eventMessages).WithStateFrom(savingNotification));
+
scope.Complete();
}
@@ -54,7 +83,17 @@ public class WebhookService : IWebhookService
Webhook? webhook = await _webhookRepository.GetAsync(key);
if (webhook is not null)
{
+ EventMessages eventMessages = _eventMessagesFactory.Get();
+ var deletingNotification = new WebhookDeletingNotification(webhook, eventMessages);
+ if (scope.Notifications.PublishCancelable(deletingNotification))
+ {
+ scope.Complete();
+ return;
+ }
+
await _webhookRepository.DeleteAsync(webhook);
+ scope.Notifications.Publish(
+ new WebhookDeletedNotification(webhook, eventMessages).WithStateFrom(deletingNotification));
}
scope.Complete();
diff --git a/src/Umbraco.Core/UdiParser.cs b/src/Umbraco.Core/UdiParser.cs
index 1442d43eb2..40a676d659 100644
--- a/src/Umbraco.Core/UdiParser.cs
+++ b/src/Umbraco.Core/UdiParser.cs
@@ -1,4 +1,4 @@
-using System.Collections.Concurrent;
+using System.Collections.Concurrent;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
@@ -231,5 +231,6 @@ public sealed class UdiParser
{ Constants.UdiEntityType.PartialView, UdiType.StringUdi },
{ Constants.UdiEntityType.PartialViewMacro, UdiType.StringUdi },
{ Constants.UdiEntityType.Stylesheet, UdiType.StringUdi },
+ { Constants.UdiEntityType.Webhook, UdiType.GuidUdi },
};
}
diff --git a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs
index de15e23a00..529fe4191b 100644
--- a/src/Umbraco.Core/Webhooks/WebhookEventBase.cs
+++ b/src/Umbraco.Core/Webhooks/WebhookEventBase.cs
@@ -1,4 +1,4 @@
-using Microsoft.Extensions.Options;
+using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Events;
diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs
index 68d668645e..5e76d54ef4 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs
@@ -1,4 +1,4 @@
-using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models;
From 31678bb67644ed1653a69bc5416d1ba5f35d1b76 Mon Sep 17 00:00:00 2001
From: Erik-Jan Westendorp
Date: Fri, 10 Nov 2023 10:41:31 +0100
Subject: [PATCH 12/36] Translate webhooks keys to Dutch (#15091)
---
src/Umbraco.Core/EmbeddedResources/Lang/en.xml | 7 +++++++
src/Umbraco.Core/EmbeddedResources/Lang/nl.xml | 8 ++++++++
2 files changed, 15 insertions(+)
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
index 4dc13c1ede..c85b1122cd 100644
--- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
+++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
@@ -1930,6 +1930,13 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Enable cleanup
NOTE! The cleanup of historically content versions are disabled globally. These settings will not take effect before it is enabled.]]>
+
+ Create webhook
+ Add webhook header
+ Logs
+ Add Document Type
+ Add Media Type
+
Add language
ISO code
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml
index 209b5e0d38..5681236051 100644
--- a/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml
+++ b/src/Umbraco.Core/EmbeddedResources/Lang/nl.xml
@@ -1706,6 +1706,13 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je
Opschonen aanzetten
Geschiedenis opschonen is globaal uitgeschakeld. Deze instellingen worden pas van kracht nadat ze zijn ingeschakeld.
+
+ Webhook aanmaken
+ Webhook header toevoegen
+ Logboek
+ Documenttype toevoegen
+ Mediatype toevoegen
+
Taal toevoegen
Verplichte taal
@@ -1846,6 +1853,7 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je
Instellingen
Sjabloon
Derde partij
+ Webhooks
Nieuwe update beschikbaar
From 81caf2b384c9772e356f0ee4d3480b9ba5a2b536 Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 10 Nov 2023 13:38:08 +0100
Subject: [PATCH 13/36] V14: make v13 login screen work initially with
Management API (#15170)
* make the login check a bit more robust to be able to handle both old postlogin and new management api response types
* v14 only: update the login url to work with the management api (this will still work with the old backoffice to some extent)
* Revert "v14 only: update the login url to work with the management api (this will still work with the old backoffice to some extent)"
This reverts commit 0639ca80f0ce620b3555b959d5ff10678730acfd.
* V14 only: additionally authenticate with the Management API to set the right cookies
---
src/Umbraco.Web.UI.Login/src/auth.element.ts | 9 +-
.../src/context/auth.context.ts | 25 +-
.../src/context/auth.repository.ts | 459 ++++++++++--------
3 files changed, 272 insertions(+), 221 deletions(-)
diff --git a/src/Umbraco.Web.UI.Login/src/auth.element.ts b/src/Umbraco.Web.UI.Login/src/auth.element.ts
index 1858bb966d..7bf9c1fa48 100644
--- a/src/Umbraco.Web.UI.Login/src/auth.element.ts
+++ b/src/Umbraco.Web.UI.Login/src/auth.element.ts
@@ -60,8 +60,6 @@ const createForm = (elements: HTMLElement[]) => {
@customElement('umb-auth')
export default class UmbAuthElement extends LitElement {
- #returnPath = '';
-
/**
* Disables the local login form and only allows external login providers.
*
@@ -89,12 +87,7 @@ export default class UmbAuthElement extends LitElement {
@property({ type: String, attribute: 'return-url' })
set returnPath(value: string) {
- this.#returnPath = value;
- umbAuthContext.returnPath = this.returnPath;
- }
- get returnPath() {
- // Check if there is a ?redir querystring or else return the returnUrl attribute
- return new URLSearchParams(window.location.search).get('returnPath') || this.#returnPath;
+ umbAuthContext.returnPath = value;
}
/**
diff --git a/src/Umbraco.Web.UI.Login/src/context/auth.context.ts b/src/Umbraco.Web.UI.Login/src/context/auth.context.ts
index 4abd9441a3..b84ecfd039 100644
--- a/src/Umbraco.Web.UI.Login/src/context/auth.context.ts
+++ b/src/Umbraco.Web.UI.Login/src/context/auth.context.ts
@@ -15,7 +15,30 @@ export class UmbAuthContext implements IUmbAuthContext {
#authRepository = new UmbAuthRepository();
- public returnPath = '';
+ #returnPath = '';
+
+ set returnPath(value: string) {
+ this.#returnPath = value;
+ }
+
+ /**
+ * Gets the return path from the query string.
+ *
+ * It will first look for a `ReturnUrl` parameter, then a `returnPath` parameter, and finally the `returnPath` property.
+ *
+ * @returns The return path from the query string.
+ */
+ get returnPath(): string {
+ const params = new URLSearchParams(window.location.search);
+ let returnUrl = params.get('ReturnUrl') ?? params.get('returnPath') ?? this.#returnPath;
+
+ // Paths from the old Backoffice are encoded twice and need to be decoded,
+ // but we don't want to decode the new paths coming from the Management API.
+ if (returnUrl.indexOf('/security/back-office/authorize') === -1) {
+ returnUrl = decodeURIComponent(returnUrl);
+ }
+ return returnUrl || '';
+ }
async login(data: LoginRequestModel): Promise {
return this.#authRepository.login(data);
diff --git a/src/Umbraco.Web.UI.Login/src/context/auth.repository.ts b/src/Umbraco.Web.UI.Login/src/context/auth.repository.ts
index 46e0ca4215..75ab8bb496 100644
--- a/src/Umbraco.Web.UI.Login/src/context/auth.repository.ts
+++ b/src/Umbraco.Web.UI.Login/src/context/auth.repository.ts
@@ -10,254 +10,289 @@ import { umbLocalizationContext } from '../external/localization/localization-co
export class UmbAuthRepository {
readonly #authURL = 'backoffice/umbracoapi/authentication/postlogin';
- public async login(data: LoginRequestModel): Promise {
- try {
- const request = new Request(this.#authURL, {
- method: 'POST',
- body: JSON.stringify({
- username: data.username,
- password: data.password,
- rememberMe: data.persist,
- }),
- headers: {
- 'Content-Type': 'application/json',
- },
- });
- const response = await fetch(request);
+ public async login(data: LoginRequestModel): Promise {
+ try {
+ const request = new Request(this.#authURL, {
+ method: 'POST',
+ body: JSON.stringify({
+ username: data.username,
+ password: data.password,
+ rememberMe: data.persist,
+ }),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ const response = await fetch(request);
- const text = await response.text();
- const responseData = JSON.parse(this.#removeAngularJSResponseData(text));
+ const responseData: LoginResponse = {
+ status: response.status
+ };
- return {
- status: response.status,
- error: response.ok ? undefined : await this.#getErrorText(response),
- data: responseData,
- twoFactorView: responseData?.twoFactorView,
- };
- } catch (error) {
- return {
- status: 500,
- error: error instanceof Error ? error.message : 'Unknown error',
- };
- }
- }
+ if (!response.ok) {
+ responseData.error = await this.#getErrorText(response);
+ return responseData;
+ }
- public async resetPassword(email: string): Promise {
- const request = new Request('backoffice/umbracoapi/authentication/PostRequestPasswordReset', {
- method: 'POST',
- body: JSON.stringify({
- email,
- }),
- headers: {
- 'Content-Type': 'application/json',
- },
- });
- const response = await fetch(request);
+ // Additionally authenticate with the Management API
+ await this.#managementApiLogin(data.username, data.password);
- return {
- status: response.status,
- error: response.ok ? undefined : await this.#getErrorText(response),
- };
- }
+ try {
+ const text = await response.text();
+ if (text) {
+ responseData.data = JSON.parse(this.#removeAngularJSResponseData(text));
+ }
+ } catch {}
- public async validatePasswordResetCode(user: string, code: string): Promise {
- const request = new Request('backoffice/umbracoapi/authentication/validatepasswordresetcode', {
- method: 'POST',
- body: JSON.stringify({
- userId: user,
- resetCode: code,
- }),
- headers: {
- 'Content-Type': 'application/json',
- },
- });
- const response = await fetch(request);
+ return {
+ status: response.status,
+ data: responseData?.data,
+ twoFactorView: responseData?.twoFactorView,
+ };
+ } catch (error) {
+ return {
+ status: 500,
+ error: error instanceof Error ? error.message : 'Unknown error',
+ };
+ }
+ }
- return {
- status: response.status,
- error: response.ok ? undefined : await this.#getErrorText(response),
- };
- }
+ public async resetPassword(email: string): Promise {
+ const request = new Request('backoffice/umbracoapi/authentication/PostRequestPasswordReset', {
+ method: 'POST',
+ body: JSON.stringify({
+ email,
+ }),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ const response = await fetch(request);
- public async newPassword(password: string, resetCode: string, userId: number): Promise {
- const request = new Request('backoffice/umbracoapi/authentication/PostSetPassword', {
- method: 'POST',
- body: JSON.stringify({
- password,
- resetCode,
- userId,
- }),
- headers: {
- 'Content-Type': 'application/json',
- },
- });
- const response = await fetch(request);
+ return {
+ status: response.status,
+ error: response.ok ? undefined : await this.#getErrorText(response),
+ };
+ }
- return {
- status: response.status,
- error: response.ok ? undefined : await this.#getErrorText(response),
- };
- }
+ public async validatePasswordResetCode(user: string, code: string): Promise {
+ const request = new Request('backoffice/umbracoapi/authentication/validatepasswordresetcode', {
+ method: 'POST',
+ body: JSON.stringify({
+ userId: user,
+ resetCode: code,
+ }),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ const response = await fetch(request);
- public async newInvitedUserPassword(newPassWord: string): Promise {
- const request = new Request('backoffice/umbracoapi/authentication/PostSetInvitedUserPassword', {
- method: 'POST',
- body: JSON.stringify({
- newPassWord,
- }),
- headers: {
- 'Content-Type': 'application/json',
- },
- });
- const response = await fetch(request);
+ return {
+ status: response.status,
+ error: response.ok ? undefined : await this.#getErrorText(response),
+ };
+ }
- return {
- status: response.status,
- error: response.ok ? undefined : await this.#getErrorText(response),
- };
- }
+ public async newPassword(password: string, resetCode: string, userId: number): Promise {
+ const request = new Request('backoffice/umbracoapi/authentication/PostSetPassword', {
+ method: 'POST',
+ body: JSON.stringify({
+ password,
+ resetCode,
+ userId,
+ }),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ const response = await fetch(request);
- public async getPasswordConfig(userId: string): Promise {
- //TODO: Add type
- const request = new Request(`backoffice/umbracoapi/authentication/GetPasswordConfig?userId=${userId}`, {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- },
- });
- const response = await fetch(request);
+ return {
+ status: response.status,
+ error: response.ok ? undefined : await this.#getErrorText(response),
+ };
+ }
- // Check if response contains AngularJS response data
- if (response.ok) {
- let text = await response.text();
- text = this.#removeAngularJSResponseData(text);
- const data = JSON.parse(text);
+ public async newInvitedUserPassword(newPassWord: string): Promise {
+ const request = new Request('backoffice/umbracoapi/authentication/PostSetInvitedUserPassword', {
+ method: 'POST',
+ body: JSON.stringify({
+ newPassWord,
+ }),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ const response = await fetch(request);
- return {
- status: response.status,
- data,
- };
- }
+ return {
+ status: response.status,
+ error: response.ok ? undefined : await this.#getErrorText(response),
+ };
+ }
- return {
- status: response.status,
- error: response.ok ? undefined : this.#getErrorText(response),
- };
- }
+ public async getPasswordConfig(userId: string): Promise {
+ //TODO: Add type
+ const request = new Request(`backoffice/umbracoapi/authentication/GetPasswordConfig?userId=${userId}`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ const response = await fetch(request);
- public async getInvitedUser(): Promise {
- //TODO: Add type
- const request = new Request('backoffice/umbracoapi/authentication/GetCurrentInvitedUser', {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- },
- });
- const response = await fetch(request);
+ // Check if response contains AngularJS response data
+ if (response.ok) {
+ let text = await response.text();
+ text = this.#removeAngularJSResponseData(text);
+ const data = JSON.parse(text);
- // Check if response contains AngularJS response data
- if (response.ok) {
- let text = await response.text();
- text = this.#removeAngularJSResponseData(text);
- const user = JSON.parse(text);
+ return {
+ status: response.status,
+ data,
+ };
+ }
- return {
- status: response.status,
- user,
- };
- }
+ return {
+ status: response.status,
+ error: response.ok ? undefined : this.#getErrorText(response),
+ };
+ }
- return {
- status: response.status,
- error: this.#getErrorText(response),
- };
- }
+ public async getInvitedUser(): Promise {
+ //TODO: Add type
+ const request = new Request('backoffice/umbracoapi/authentication/GetCurrentInvitedUser', {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ const response = await fetch(request);
- public async getMfaProviders(): Promise {
- const request = new Request('backoffice/umbracoapi/authentication/Get2faProviders', {
- method: 'GET',
- headers: {
- 'Content-Type': 'application/json',
- },
- });
- const response = await fetch(request);
+ // Check if response contains AngularJS response data
+ if (response.ok) {
+ let text = await response.text();
+ text = this.#removeAngularJSResponseData(text);
+ const user = JSON.parse(text);
- // Check if response contains AngularJS response data
- if (response.ok) {
- let text = await response.text();
- text = this.#removeAngularJSResponseData(text);
- const providers = JSON.parse(text);
+ return {
+ status: response.status,
+ user,
+ };
+ }
- return {
- status: response.status,
- providers,
- };
- }
+ return {
+ status: response.status,
+ error: this.#getErrorText(response),
+ };
+ }
- return {
- status: response.status,
- error: await this.#getErrorText(response),
- providers: [],
- };
- }
+ public async getMfaProviders(): Promise {
+ const request = new Request('backoffice/umbracoapi/authentication/Get2faProviders', {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ const response = await fetch(request);
- public async validateMfaCode(code: string, provider: string): Promise {
- const request = new Request('backoffice/umbracoapi/authentication/PostVerify2faCode', {
- method: 'POST',
- body: JSON.stringify({
- code,
- provider,
- }),
- headers: {
- 'Content-Type': 'application/json',
- },
- });
+ // Check if response contains AngularJS response data
+ if (response.ok) {
+ let text = await response.text();
+ text = this.#removeAngularJSResponseData(text);
+ const providers = JSON.parse(text);
- const response = await fetch(request);
+ return {
+ status: response.status,
+ providers,
+ };
+ }
+
+ return {
+ status: response.status,
+ error: await this.#getErrorText(response),
+ providers: [],
+ };
+ }
+
+ public async validateMfaCode(code: string, provider: string): Promise {
+ const request = new Request('backoffice/umbracoapi/authentication/PostVerify2faCode', {
+ method: 'POST',
+ body: JSON.stringify({
+ code,
+ provider,
+ }),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ const response = await fetch(request);
let text = await response.text();
text = this.#removeAngularJSResponseData(text);
const data = JSON.parse(text);
- if (response.ok) {
- return {
+ if (response.ok) {
+ return {
data,
- status: response.status,
- };
- }
+ status: response.status,
+ };
+ }
- return {
- status: response.status,
- error: data.Message ?? 'An unknown error occurred.',
- };
- }
+ return {
+ status: response.status,
+ error: data.Message ?? 'An unknown error occurred.',
+ };
+ }
- async #getErrorText(response: Response): Promise {
- switch (response.status) {
- case 400:
- case 401:
- return umbLocalizationContext.localize('login_userFailedLogin', undefined, "Oops! We couldn't log you in. Please check your credentials and try again.");
+ async #getErrorText(response: Response): Promise {
+ switch (response.status) {
+ case 400:
+ case 401:
+ return umbLocalizationContext.localize('login_userFailedLogin', undefined, "Oops! We couldn't log you in. Please check your credentials and try again.");
- case 402:
- return umbLocalizationContext.localize('login_2faText', undefined, 'You have enabled 2-factor authentication and must verify your identity.');
+ case 402:
+ return umbLocalizationContext.localize('login_2faText', undefined, 'You have enabled 2-factor authentication and must verify your identity.');
- case 500:
- return umbLocalizationContext.localize('errors_receivedErrorFromServer', undefined, 'Received error from server');
+ case 500:
+ return umbLocalizationContext.localize('errors_receivedErrorFromServer', undefined, 'Received error from server');
- default:
- return response.statusText ?? await umbLocalizationContext.localize('errors_receivedErrorFromServer', undefined, 'Received error from server')
- }
- }
+ default:
+ return response.statusText ?? await umbLocalizationContext.localize('errors_receivedErrorFromServer', undefined, 'Received error from server')
+ }
+ }
- /**
- * AngularJS adds a prefix to the response data, which we need to remove
- */
- #removeAngularJSResponseData(text: string) {
- if (text.startsWith(")]}',\n")) {
- text = text.split('\n')[1];
- }
+ /**
+ * AngularJS adds a prefix to the response data, which we need to remove
+ */
+ #removeAngularJSResponseData(text: string) {
+ if (text.startsWith(")]}',\n")) {
+ text = text.split('\n')[1];
+ }
- return text;
- }
+ return text;
+ }
+
+ async #managementApiLogin(username: string, password: string) {
+ try {
+ const authURLManagementApi = 'management/api/v1/security/back-office/login';
+ const requestManagementApi = new Request(authURLManagementApi, {
+ method: 'POST',
+ body: JSON.stringify({
+ username,
+ password,
+ }),
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ return await fetch(requestManagementApi);
+ } catch (error) {
+ console.error('Failed to authenticate with the Management API:', error);
+ }
+ }
}
From f4cb7e71fa5b35de6d744bf73e8b5c48d697627f Mon Sep 17 00:00:00 2001
From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com>
Date: Fri, 10 Nov 2023 13:45:00 +0100
Subject: [PATCH 14/36] Cherry-picked c03df2b74ed120246768d1d8803a99612e4386f3
into v13/dev
---
.../src/context/auth.repository.ts | 23 -------------------
1 file changed, 23 deletions(-)
diff --git a/src/Umbraco.Web.UI.Login/src/context/auth.repository.ts b/src/Umbraco.Web.UI.Login/src/context/auth.repository.ts
index 75ab8bb496..24c1cfe015 100644
--- a/src/Umbraco.Web.UI.Login/src/context/auth.repository.ts
+++ b/src/Umbraco.Web.UI.Login/src/context/auth.repository.ts
@@ -34,9 +34,6 @@ export class UmbAuthRepository {
return responseData;
}
- // Additionally authenticate with the Management API
- await this.#managementApiLogin(data.username, data.password);
-
try {
const text = await response.text();
if (text) {
@@ -275,24 +272,4 @@ export class UmbAuthRepository {
return text;
}
-
- async #managementApiLogin(username: string, password: string) {
- try {
- const authURLManagementApi = 'management/api/v1/security/back-office/login';
- const requestManagementApi = new Request(authURLManagementApi, {
- method: 'POST',
- body: JSON.stringify({
- username,
- password,
- }),
- headers: {
- 'Content-Type': 'application/json',
- },
- });
-
- return await fetch(requestManagementApi);
- } catch (error) {
- console.error('Failed to authenticate with the Management API:', error);
- }
- }
}
From 7c877431912178a0938296679f638568a8cbbeef Mon Sep 17 00:00:00 2001
From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
Date: Fri, 10 Nov 2023 14:14:32 +0100
Subject: [PATCH 15/36] Add Auth attribute to controller (#15178)
---
src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs
index 5e76d54ef4..afa39ff623 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs
@@ -1,3 +1,4 @@
+using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Mapping;
@@ -6,11 +7,13 @@ using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Webhooks;
using Umbraco.Cms.Web.BackOffice.Services;
using Umbraco.Cms.Web.Common.Attributes;
+using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Cms.Web.Common.Models;
namespace Umbraco.Cms.Web.BackOffice.Controllers;
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
+[Authorize(Policy = AuthorizationPolicies.TreeAccessWebhooks)]
public class WebhookController : UmbracoAuthorizedJsonController
{
private readonly IWebhookService _webhookService;
From e48e7ff1464e66d0d5f1f46020aecf3ca4528849 Mon Sep 17 00:00:00 2001
From: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
Date: Mon, 13 Nov 2023 09:11:11 +0100
Subject: [PATCH 16/36] Add [SpecialType] attributes to request & response
(#15103)
Co-authored-by: Zeegaan
---
src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs
index a8606c7391..7637ec4dc3 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/WebhookLogDto.cs
@@ -42,18 +42,22 @@ internal class WebhookLogDto
public int RetryCount { get; set; }
[Column(Name = "requestHeaders")]
+ [SpecialDbType(SpecialDbTypes.NVARCHARMAX)]
[NullSetting(NullSetting = NullSettings.NotNull)]
public string RequestHeaders { get; set; } = string.Empty;
[Column(Name = "requestBody")]
+ [SpecialDbType(SpecialDbTypes.NVARCHARMAX)]
[NullSetting(NullSetting = NullSettings.NotNull)]
public string RequestBody { get; set; } = string.Empty;
[Column(Name = "responseHeaders")]
+ [SpecialDbType(SpecialDbTypes.NVARCHARMAX)]
[NullSetting(NullSetting = NullSettings.NotNull)]
public string ResponseHeaders { get; set; } = string.Empty;
[Column(Name = "responseBody")]
+ [SpecialDbType(SpecialDbTypes.NVARCHARMAX)]
[NullSetting(NullSetting = NullSettings.NotNull)]
public string ResponseBody { get; set; } = string.Empty;
}
From 1d43a67934323e89ce5a836c2bf57967fd881df6 Mon Sep 17 00:00:00 2001
From: Bjarne Fyrstenborg
Date: Mon, 13 Nov 2023 10:14:40 +0100
Subject: [PATCH 17/36] Add edit page for webhook (#15175)
* Add edit page for webhook
* Remove function again
* Remove old edit in overlay
* Update language keys
* Update GetByKey to get model as camel case properties
* Localize "select event"
* Localize "create header"
* Don't show input field in header for now
* Add generic class to limit property editor width
* Remove loading check since already checked when loading entire editor view
* Add empty message
* Update col span
* Set edit name
* Map id property
* Return webhook model after update
* Translate speech bubble text
* Resolve content types
* Only push if not already exists
* Check webhook headers
* Set webhook enabled by default
---
.../EmbeddedResources/Lang/da.xml | 10 +-
.../EmbeddedResources/Lang/en.xml | 11 +-
.../EmbeddedResources/Lang/en_us.xml | 11 +-
.../Controllers/WebhookController.cs | 11 +-
.../Services/WebhookPresentationFactory.cs | 18 +-
.../Models/WebhookViewModel.cs | 5 +-
.../src/less/property-editors.less | 6 +-
.../src/views/languages/edit.controller.js | 2 +-
.../views/languages/overview.controller.js | 1 +
.../src/views/webhooks/edit.controller.js | 330 ++++++++++++++++++
.../src/views/webhooks/edit.html | 206 +++++++++++
.../webhooks/overlays/edit.controller.js | 146 --------
.../src/views/webhooks/overlays/edit.html | 159 ---------
.../src/views/webhooks/webhooks.controller.js | 89 +----
.../src/views/webhooks/webhooks.html | 4 +-
15 files changed, 603 insertions(+), 406 deletions(-)
create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/edit.controller.js
create mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/edit.html
delete mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js
delete mode 100644 src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml
index fa21b1e377..3e01c0de9b 100644
--- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml
+++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml
@@ -477,9 +477,10 @@
Er du sikker på at du vil forlade Umbraco?
Er du sikker?
Klip
- Rediger ordbogsnøgle
- Rediger sprog
+ Rediger ordbogsnøgle
+ Rediger sprog
Rediger det valgte medie
+ Edit webhook
Indsæt lokalt link
Indsæt tegn
Indsæt grafisk overskrift
@@ -531,6 +532,7 @@
Åben linket i et nyt vindue eller fane
Link til medie
Vælg startnode for indhold
+ Vælg event
Vælg medie
Vælg medietype
Vælg ikon
@@ -1009,6 +1011,9 @@
Tilføj webhook header
Tilføj dokument type
Tilføj medie Type
+ Opret header
+ Logs
+ Der er ikke tilføjet nogen webhook headers
Culture Code
@@ -1463,6 +1468,7 @@ Mange hilsner fra Umbraco robotten
Dit systems information er blevet kopieret til udklipsholderen
Kunne desværre ikke kopiere dit systems information til udklipsholderen
+ Webhook gemt
Tilføj style
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
index c85b1122cd..e6662d6201 100644
--- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
+++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
@@ -486,9 +486,10 @@
Are you sure?
Are you sure?
Cut
- Edit Dictionary Item
- Edit Language
+ Edit dictionary item
+ Edit language
Edit selected media
+ Edit webhook
Insert local link
Insert character
Insert graphic headline
@@ -542,6 +543,7 @@
Opens the linked document in a new window or tab
Link to media
Select content start node
+ Select event
Select media
Select media type
Select icon
@@ -1666,6 +1668,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Your system information has successfully been copied to the clipboard
Could not copy your system information to the clipboard
+ Webhook saved
Add style
@@ -1933,9 +1936,11 @@ 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
+ Create header
+ Logs
+ No webhook headers have been added
Add language
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml
index 77272aa79b..0602ff487a 100644
--- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml
+++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml
@@ -501,9 +501,10 @@
Are you sure?
Are you sure?
Cut
- Edit Dictionary Item
- Edit Language
+ Edit dictionary item
+ Edit language
Edit selected media
+ Edit webhook
Insert local link
Insert character
Insert graphic headline
@@ -557,6 +558,7 @@
Opens the linked document in a new window or tab
Link to media
Select content start node
+ Select event
Select media
Select media type
Select icon
@@ -1731,6 +1733,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
An error occurred while disabling version cleanup for %0%
Your system information has successfully been copied to the clipboard
Could not copy your system information to the clipboard
+ Webhook saved
Add style
@@ -2015,9 +2018,11 @@ 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
+ Create header
+ Logs
+ No webhook headers have been added
Add language
diff --git a/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs b/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs
index afa39ff623..9db46330ed 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/WebhookController.cs
@@ -44,20 +44,21 @@ public class WebhookController : UmbracoAuthorizedJsonController
[HttpPut]
public async Task Update(WebhookViewModel webhookViewModel)
{
- Webhook updateModel = _umbracoMapper.Map(webhookViewModel)!;
+ Webhook webhook = _umbracoMapper.Map(webhookViewModel)!;
- await _webhookService.UpdateAsync(updateModel);
+ await _webhookService.UpdateAsync(webhook);
- return Ok();
+ return Ok(_webhookPresentationFactory.Create(webhook));
}
[HttpPost]
public async Task Create(WebhookViewModel webhookViewModel)
{
Webhook webhook = _umbracoMapper.Map(webhookViewModel)!;
+
await _webhookService.CreateAsync(webhook);
- return Ok();
+ return Ok(_webhookPresentationFactory.Create(webhook));
}
[HttpGet]
@@ -65,7 +66,7 @@ public class WebhookController : UmbracoAuthorizedJsonController
{
Webhook? webhook = await _webhookService.GetAsync(key);
- return webhook is null ? NotFound() : Ok(webhook);
+ return webhook is null ? NotFound() : Ok(_webhookPresentationFactory.Create(webhook));
}
[HttpDelete]
diff --git a/src/Umbraco.Web.BackOffice/Services/WebhookPresentationFactory.cs b/src/Umbraco.Web.BackOffice/Services/WebhookPresentationFactory.cs
index 57ef7e5f2a..ef4e052596 100644
--- a/src/Umbraco.Web.BackOffice/Services/WebhookPresentationFactory.cs
+++ b/src/Umbraco.Web.BackOffice/Services/WebhookPresentationFactory.cs
@@ -1,4 +1,4 @@
-using Umbraco.Cms.Core;
+using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Webhooks;
using Umbraco.Cms.Web.Common.Models;
@@ -14,12 +14,15 @@ internal class WebhookPresentationFactory : IWebhookPresentationFactory
public WebhookViewModel Create(Webhook webhook)
{
var target = new WebhookViewModel
- {
- ContentTypeKeys = webhook.ContentTypeKeys, Events = webhook.Events.Select(Create).ToArray(), Url = webhook.Url,
- Enabled = webhook.Enabled,
- Key = webhook.Key,
- Headers = webhook.Headers,
- };
+ {
+ ContentTypeKeys = webhook.ContentTypeKeys,
+ Events = webhook.Events.Select(Create).ToArray(),
+ Url = webhook.Url,
+ Enabled = webhook.Enabled,
+ Id = webhook.Id,
+ Key = webhook.Key,
+ Headers = webhook.Headers,
+ };
return target;
}
@@ -27,6 +30,7 @@ internal class WebhookPresentationFactory : IWebhookPresentationFactory
private WebhookEventViewModel Create(string alias)
{
IWebhookEvent? webhookEvent = _webhookEventCollection.FirstOrDefault(x => x.Alias == alias);
+
return new WebhookEventViewModel
{
EventName = webhookEvent?.EventName ?? alias,
diff --git a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs
index 9030ff050c..a1e581ff22 100644
--- a/src/Umbraco.Web.Common/Models/WebhookViewModel.cs
+++ b/src/Umbraco.Web.Common/Models/WebhookViewModel.cs
@@ -1,10 +1,13 @@
-using System.Runtime.Serialization;
+using System.Runtime.Serialization;
namespace Umbraco.Cms.Web.Common.Models;
[DataContract]
public class WebhookViewModel
{
+ [DataMember(Name = "id")]
+ public int Id { get; set; }
+
[DataMember(Name = "key")]
public Guid? Key { get; set; }
diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less
index d68a48c702..f24365a806 100644
--- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less
+++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less
@@ -2,7 +2,11 @@
// Container styles
// --------------------------------------------------
.umb-property-editor {
- width: 100%;
+ width: 100%;
+
+ &--limit-width {
+ .umb-property-editor--limit-width();
+ }
}
.umb-property-editor-tiny {
diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js
index b545bd2a7c..b2b5f487be 100644
--- a/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/languages/edit.controller.js
@@ -1,7 +1,7 @@
(function () {
"use strict";
- function LanguagesEditController($scope, $q, $timeout, $location, $routeParams, overlayService, navigationService, notificationsService, localizationService, languageResource, contentEditingHelper, formHelper, eventsService) {
+ function LanguagesEditController($scope, $q, $timeout, $location, $routeParams, overlayService, navigationService, notificationsService, localizationService, languageResource, formHelper, eventsService) {
var vm = this;
vm.isNew = false;
diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js
index ef46bcf84d..ef30ca6d46 100644
--- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.controller.js
@@ -11,6 +11,7 @@
vm.addLanguage = addLanguage;
vm.editLanguage = editLanguage;
vm.deleteLanguage = deleteLanguage;
+
vm.getLanguageById = function(id) {
for (var i = 0; i < vm.languages.length; i++) {
if (vm.languages[i].id === id) {
diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.controller.js
new file mode 100644
index 0000000000..93bbd842b0
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.controller.js
@@ -0,0 +1,330 @@
+(function () {
+ "use strict";
+
+ function WebhooksEditController($scope, $q, $timeout, $location, $routeParams, editorService, eventsService, navigationService, notificationsService, localizationService, contentTypeResource, mediaTypeResource, memberTypeResource, webhooksResource, formHelper) {
+
+ const vm = this;
+
+ vm.isNew = false;
+ vm.showIdentifier = true;
+
+ vm.contentTypes = [];
+ vm.webhook = {};
+ vm.breadcrumbs = [];
+ vm.labels = {};
+ vm.save = save;
+ vm.back = back;
+ vm.goToPage = goToPage;
+
+ vm.clearContentType = clearContentType;
+ vm.clearEvent = clearEvent;
+ vm.openContentTypePicker = openContentTypePicker;
+ vm.openEventPicker = openEventPicker;
+ vm.openCreateHeader = openCreateHeader;
+ vm.removeHeader = removeHeader;
+
+ function init() {
+ vm.loading = true;
+
+ let promises = [];
+
+ // Localize labels
+ promises.push(localizationService.localizeMany([
+ "treeHeaders_webhooks",
+ "webhooks_addWebhook",
+ "defaultdialogs_confirmSure",
+ "defaultdialogs_editWebhook"
+ ]).then(function (values) {
+ vm.labels.webhooks = values[0];
+ vm.labels.addWebhook = values[1];
+ vm.labels.areYouSure = values[2];
+ vm.labels.editWebhook = values[3];
+
+ if ($routeParams.create) {
+ vm.isNew = true;
+ vm.showIdentifier = false;
+ vm.webhook.name = vm.labels.addWebhook;
+ vm.webhook.enabled = true;
+ }
+ }));
+
+ // Load events
+ promises.push(loadEvents());
+
+ if (!$routeParams.create) {
+
+ promises.push(webhooksResource.getByKey($routeParams.id).then(webhook => {
+
+ vm.webhook = webhook;
+ vm.webhook.name = vm.labels.editWebhook;
+
+ const eventType = vm.webhook ? vm.webhook.events[0].eventType.toLowerCase() : null;
+ const contentTypes = webhook.contentTypeKeys.map(x => ({ key: x }));
+
+ getEntities(contentTypes, eventType);
+
+ makeBreadcrumbs();
+ }));
+ }
+
+ $q.all(promises).then(() => {
+ if ($routeParams.create) {
+ $scope.$emit("$changeTitle", vm.labels.addWebhook);
+ } else {
+ $scope.$emit("$changeTitle", vm.labels.editWebhook + ": " + vm.webhook.key);
+ }
+
+ vm.loading = false;
+ });
+
+ // Activate tree node
+ $timeout(function () {
+ navigationService.syncTree({ tree: $routeParams.tree, path: [-1], activate: true });
+ });
+ }
+
+ function openEventPicker() {
+
+ const dialog = {
+ selectedEvents: vm.webhook.events,
+ submit(model) {
+ vm.webhook.events = model.selection;
+ editorService.close();
+ },
+ close() {
+ editorService.close();
+ }
+ };
+
+ localizationService.localize("defaultdialogs_selectEvent").then(value => {
+ dialog.title = value;
+ editorService.eventPicker(dialog);
+ });
+ }
+
+ function openContentTypePicker() {
+ const eventType = vm.webhook ? vm.webhook.events[0].eventType.toLowerCase() : null;
+
+ const editor = {
+ multiPicker: true,
+ filterCssClass: "not-allowed not-published",
+ filter: function (item) {
+ // filter out folders (containers), element types (for content) and already selected items
+ return item.nodeType === "container"; // || item.metaData.isElement || !!_.findWhere(vm.itemTypes, { udi: item.udi });
+ },
+ submit(model) {
+ getEntities(model.selection, eventType);
+ vm.webhook.contentTypeKeys = model.selection.map(item => item.key);
+ editorService.close();
+ },
+ close() {
+ editorService.close();
+ }
+ };
+
+ switch (eventType.toLowerCase()) {
+ case "content":
+ editorService.contentTypePicker(editor);
+ break;
+ case "media":
+ editorService.mediaTypePicker(editor);
+ break;
+ case "member":
+ editorService.memberTypePicker(editor);
+ break;
+ }
+ }
+
+ function getEntities(selection, eventType) {
+ let resource;
+ switch (eventType.toCamelCase()) {
+ case "content":
+ resource = contentTypeResource;
+ break;
+ case "media":
+ resource = mediaTypeResource;
+ break;
+ case "member":
+ resource = memberTypeResource;
+ break;
+ default:
+ return;
+ }
+
+ selection.forEach(entity => {
+ resource.getById(entity.key)
+ .then(data => {
+ if (!vm.contentTypes.some(x => x.key === data.key)) {
+ vm.contentTypes.push(data);
+ }
+ });
+ });
+ }
+
+ function loadEvents() {
+ return webhooksResource.getAllEvents()
+ .then(data => {
+ vm.events = data;
+ });
+ }
+
+ function clearContentType(contentTypeKey) {
+ if (Utilities.isArray(vm.webhook.contentTypeKeys)) {
+ vm.webhook.contentTypeKeys = vm.webhook.contentTypeKeys.filter(x => x !== contentTypeKey);
+ }
+ if (Utilities.isArray(vm.contentTypes)) {
+ vm.contentTypes = vm.contentTypes.filter(x => x.key !== contentTypeKey);
+ }
+ }
+
+ function clearEvent(event) {
+ if (Utilities.isArray(vm.webhook.events)) {
+ vm.webhook.events = vm.webhook.events.filter(x => x !== event);
+ }
+
+ if (Utilities.isArray(vm.contentTypes)) {
+ vm.events = vm.events.filter(x => x.key !== event);
+ }
+ }
+
+ function openCreateHeader() {
+
+ const dialog = {
+ view: "views/webhooks/overlays/header.html",
+ size: 'small',
+ position: 'right',
+ submit(model) {
+ if (!vm.webhook.headers) {
+ vm.webhook.headers = {};
+ }
+ vm.webhook.headers[model.key] = model.value;
+ editorService.close();
+ },
+ close() {
+ editorService.close();
+ }
+ };
+
+ localizationService.localize("webhooks_createHeader").then(value => {
+ dialog.title = value;
+ editorService.open(dialog);
+ });
+ }
+
+ function removeHeader(key) {
+ delete vm.webhook.headers[key];
+ }
+
+ function save() {
+
+ if (!formHelper.submitForm({ scope: $scope })) {
+ return;
+ }
+
+ saveWebhook();
+ }
+
+ function saveWebhook() {
+
+ if (vm.isNew) {
+ webhooksResource.create(vm.webhook)
+ .then(webhook => {
+
+ formHelper.resetForm({ scope: $scope });
+
+ vm.webhook = webhook;
+ vm.webhook.name = vm.labels.editWebhook;
+
+ vm.saveButtonState = "success";
+
+ $scope.$emit("$changeTitle", vm.labels.editWebhook + ": " + vm.webhook.key);
+
+ localizationService.localize("speechBubbles_webhookSaved").then(value => {
+ notificationsService.success(value);
+ });
+
+ // Emit event when language is created or updated/saved
+ eventsService.emit("editors.webhooks.webhookSaved", {
+ webhook: webhook,
+ isNew: vm.isNew
+ });
+
+ vm.isNew = false;
+ vm.showIdentifier = true;
+
+ }, x => {
+ let errorMessage = undefined;
+ if (x.data.ModelState) {
+ errorMessage = `Message: ${Object.values(x.data.ModelState).flat().join(' ')}`;
+ }
+ handleSubmissionError(x, `Error saving webhook. ${errorMessage ?? ''}`);
+ });
+ }
+ else {
+ webhooksResource.update(vm.webhook)
+ .then(webhook => {
+
+ formHelper.resetForm({ scope: $scope });
+
+ vm.webhook = webhook;
+ vm.webhook.name = vm.labels.editWebhook;
+
+ vm.saveButtonState = "success";
+
+ $scope.$emit("$changeTitle", vm.labels.editWebhook + ": " + vm.webhook.key);
+
+ localizationService.localize("speechBubbles_webhookSaved").then(value => {
+ notificationsService.success(value);
+ });
+
+ // Emit event when language is created or updated/saved
+ eventsService.emit("editors.webhooks.webhookSaved", {
+ webhook: webhook,
+ isNew: vm.isNew
+ });
+
+ vm.isNew = false;
+ vm.showIdentifier = true;
+
+ }, x => {
+ let errorMessage = undefined;
+ if (x.data.ModelState) {
+ errorMessage = `Message: ${Object.values(x.data.ModelState).flat().join(' ')}`;
+ }
+ handleSubmissionError(x, `Error saving webhook. ${errorMessage ?? ''}`);
+ });
+ }
+ }
+
+ function handleSubmissionError(err, errorMessage) {
+ notificationsService.error(errorMessage);
+ vm.saveButtonState = 'error';
+ formHelper.resetForm({ scope: $scope, hasErrors: true });
+ formHelper.handleError(err);
+ }
+
+ function back() {
+ $location.path("settings/webhooks/overview");
+ }
+
+ function goToPage(ancestor) {
+ $location.path(ancestor.path);
+ }
+
+ function makeBreadcrumbs() {
+ vm.breadcrumbs = [
+ {
+ "name": vm.labels.webhooks,
+ "path": "/settings/webhooks/overview"
+ },
+ {
+ "name": vm.webhook.name
+ }
+ ];
+ }
+
+ init();
+ }
+
+ angular.module("umbraco").controller("Umbraco.Editors.Webhooks.EditController", WebhooksEditController);
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.html b/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.html
new file mode 100644
index 0000000000..66f93c244e
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/edit.html
@@ -0,0 +1,206 @@
+
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
deleted file mode 100644
index 6f8a17ceb8..0000000000
--- a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.controller.js
+++ /dev/null
@@ -1,146 +0,0 @@
-(function () {
- "use strict";
-
- function EditController($scope, editorService, contentTypeResource, mediaTypeResource, memberTypeResource) {
-
- const vm = this;
-
- vm.clearContentType = clearContentType;
- vm.clearEvent = clearEvent;
- vm.removeHeader = removeHeader;
- vm.openCreateHeader = openCreateHeader;
- vm.openEventPicker = openEventPicker;
- vm.openContentTypePicker = openContentTypePicker;
- vm.close = close;
- vm.submit = submit;
-
- function openEventPicker() {
- editorService.eventPicker({
- title: "Select event",
- selectedEvents: $scope.model.webhook.events,
- submit(model) {
- $scope.model.webhook.events = model.selection;
- editorService.close();
- },
- close() {
- editorService.close();
- }
- });
- }
-
- function openContentTypePicker() {
- const eventType = $scope.model.webhook ? $scope.model.webhook.events[0].eventType.toLowerCase() : null;
-
- const editor = {
- multiPicker: true,
- filterCssClass: "not-allowed not-published",
- filter: function (item) {
- // filter out folders (containers), element types (for content) and already selected items
- return item.nodeType === "container"; // || item.metaData.isElement || !!_.findWhere(vm.itemTypes, { udi: item.udi });
- },
- submit(model) {
- getEntities(model.selection, eventType);
- $scope.model.webhook.contentTypeKeys = model.selection.map(item => item.key);
- editorService.close();
- },
- close() {
- editorService.close();
- }
- };
-
- switch (eventType.toLowerCase()) {
- case "content":
- editorService.contentTypePicker(editor);
- break;
- case "media":
- editorService.mediaTypePicker(editor);
- break;
- case "member":
- editorService.memberTypePicker(editor);
- break;
- }
- }
-
- function openCreateHeader() {
- editorService.open({
- title: "Create header",
- view: "views/webhooks/overlays/header.html",
- 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();
- },
- close() {
- editorService.close();
- }
- });
- }
-
- function getEntities(selection, eventType) {
- let resource;
- switch (eventType.toCamelCase()) {
- case "content":
- resource = contentTypeResource;
- break;
- case "media":
- resource = mediaTypeResource;
- break;
- case "member":
- resource = memberTypeResource;
- break;
- default:
- return;
- }
-
- $scope.model.contentTypes = [];
-
- selection.forEach(entity => {
- resource.getById(entity.key)
- .then(data => {
- $scope.model.contentTypes.push(data);
- });
- });
- }
-
- function clearContentType(contentTypeKey) {
- if (Utilities.isArray($scope.model.webhook.contentTypeKeys)) {
- $scope.model.webhook.contentTypeKeys = $scope.model.webhook.contentTypeKeys.filter(x => x !== contentTypeKey);
- }
- if (Utilities.isArray($scope.model.contentTypes)) {
- $scope.model.contentTypes = $scope.model.contentTypes.filter(x => x.key !== contentTypeKey);
- }
- }
-
- function clearEvent(event) {
- if (Utilities.isArray($scope.model.webhook.events)) {
- $scope.model.webhook.events = $scope.model.webhook.events.filter(x => x !== event);
- }
-
- if (Utilities.isArray($scope.model.contentTypes)) {
- $scope.model.events = $scope.model.events.filter(x => x.key !== event);
- }
- }
-
- function removeHeader(key) {
- delete $scope.model.webhook.headers[key];
- }
-
- function close() {
- if ($scope.model.close) {
- $scope.model.close();
- }
- }
-
- function 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
deleted file mode 100644
index 4bc61fec98..0000000000
--- a/src/Umbraco.Web.UI.Client/src/views/webhooks/overlays/edit.html
+++ /dev/null
@@ -1,159 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- | Name |
- Value |
-
-
-
-
- | {{key}} |
- {{value}} |
-
-
-
- |
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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 cf85f0df2c..10a0f51c08 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
@@ -1,11 +1,12 @@
-(function () {
+(function () {
"use strict";
- function WebhookController($q, $timeout, $routeParams, webhooksResource, navigationService, notificationsService, editorService, overlayService, contentTypeResource, mediaTypeResource, memberTypeResource) {
+ function WebhookController($q, $timeout, $location, $routeParams, webhooksResource, navigationService, notificationsService, editorService, overlayService, contentTypeResource, mediaTypeResource, memberTypeResource) {
const vm = this;
- vm.openWebhookOverlay = openWebhookOverlay;
+ vm.addWebhook = addWebhook;
+ vm.editWebhook = editWebhook;
vm.deleteWebhook = deleteWebhook;
vm.handleSubmissionError = handleSubmissionError;
vm.resolveTypeNames = resolveTypeNames;
@@ -71,20 +72,6 @@
return resource;
}
- function getEntities(webhook) {
- let resource = determineResource(webhook.events[0].eventType.toLowerCase());
- let entities = [];
-
- webhook.contentTypeKeys.forEach(key => {
- resource.getById(key)
- .then(data => {
- entities.push(data);
- });
- });
-
- return entities;
- }
-
function resolveTypeNames(webhook) {
let resource = determineResource(webhook.events[0].eventType.toLowerCase());
@@ -104,70 +91,20 @@
});
}
- function handleSubmissionError (model, errorMessage) {
+ 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: "views/webhooks/overlays/edit.html",
- events: vm.events,
- contentTypes : webhook ? getEntities(webhook) : null,
- webhook: webhook ? webhook : {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.events || model.webhook.events.length === 0) {
- handleSubmissionError(model, 'Please provide the event for which the webhook should trigger');
- return;
- }
+ function addWebhook() {
+ $location.search('create', null);
+ $location.path("/settings/webhooks/edit/-1").search("create", "true");
+ }
- if (isCreating) {
- webhooksResource.create(model.webhook)
- .then(() => {
- loadWebhooks()
- 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(() => {
- loadWebhooks()
- 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 editWebhook(webhook) {
+ $location.search('create', null);
+ $location.path("/settings/webhooks/edit/" + webhook.key);
}
function loadWebhooks(){
@@ -185,7 +122,7 @@
});
}
- function deleteWebhook (webhook, event) {
+ function deleteWebhook(webhook, event) {
overlayService.open({
title: 'Confirm delete webhook',
content: 'Are you sure you want to delete the 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 9b2c1f7461..70d2a0be05 100644
--- a/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.html
+++ b/src/Umbraco.Web.UI.Client/src/views/webhooks/webhooks.html
@@ -6,7 +6,7 @@
@@ -24,7 +24,7 @@
-
+
|
Date: Thu, 2 Nov 2023 12:15:14 +0000
Subject: [PATCH 18/36] Refactor hostedServices into background jobs (#14291)
* Refactor jobs from HostedServices into BackgroundJobs
* Clean up generics and DI setup
* Add RecurringBackgroundJob Unit Tests
* Add ServiceCollection helper
* Add Obsolete attributes
* Add Notification Classes
* Add UnitTests for RecurringBackgroundJob HostedService
* Add NotificationEvents
* Add state to notifications
* Update UnitTests
* Add Obsolete Attributes to old hosted service classes
* Updated xmldoc in IRecurringBackgroundJob.cs
* Update Obsolete attribute messages to indicate classes will be removed in Umbraco 14
(cherry picked from commit c30ffa9ac3ae9c12508242855c63208567c55eb9)
---
.../BackgroundJobs/DelayCalculator.cs | 67 ++++++
.../BackgroundJobs/IRecurringBackgroundJob.cs | 28 +++
.../Jobs/ContentVersionCleanupJob.cs | 66 ++++++
.../Jobs/HealthCheckNotifierJob.cs | 116 ++++++++++
.../BackgroundJobs/Jobs/KeepAliveJob.cs | 90 ++++++++
.../BackgroundJobs/Jobs/LogScrubberJob.cs | 73 ++++++
.../BackgroundJobs/Jobs/ReportSiteJob.cs | 92 ++++++++
.../Jobs/ScheduledPublishingJob.cs | 105 +++++++++
.../InstructionProcessJob.cs | 59 +++++
.../Jobs/ServerRegistration/TouchServerJob.cs | 102 +++++++++
.../BackgroundJobs/Jobs/TempFileCleanupJob.cs | 99 +++++++++
.../RecurringBackgroundJobHostedService.cs | 127 +++++++++++
...curringBackgroundJobHostedServiceRunner.cs | 81 +++++++
.../Extensions/ServiceCollectionExtensions.cs | 31 +++
.../HostedServices/ContentVersionCleanup.cs | 1 +
.../HostedServices/HealthCheckNotifier.cs | 1 +
.../HostedServices/KeepAlive.cs | 1 +
.../HostedServices/LogScrubber.cs | 1 +
.../RecurringHostedServiceBase.cs | 4 +-
.../HostedServices/ReportSiteTask.cs | 1 +
.../HostedServices/ScheduledPublishing.cs | 1 +
.../InstructionProcessTask.cs | 1 +
.../ServerRegistration/TouchServerTask.cs | 1 +
.../HostedServices/TempFileCleanup.cs | 1 +
...urringBackgroundJobExecutedNotification.cs | 12 +
...rringBackgroundJobExecutingNotification.cs | 12 +
...ecurringBackgroundJobFailedNotification.cs | 12 +
...curringBackgroundJobIgnoredNotification.cs | 12 +
.../RecurringBackgroundJobNotification.cs | 12 +
...curringBackgroundJobStartedNotification.cs | 12 +
...urringBackgroundJobStartingNotification.cs | 12 +
...curringBackgroundJobStoppedNotification.cs | 12 +
...urringBackgroundJobStoppingNotification.cs | 12 +
.../UmbracoBuilderExtensions.cs | 2 +-
.../UmbracoBuilderExtensions.cs | 35 +++
.../Jobs/HealthCheckNotifierJobTests.cs | 139 ++++++++++++
.../BackgroundJobs/Jobs/KeepAliveJobTests.cs | 94 ++++++++
.../Jobs/LogScrubberJobTests.cs | 72 ++++++
.../Jobs/ScheduledPublishingJobTests.cs | 92 ++++++++
.../InstructionProcessJobTests.cs | 51 +++++
.../ServerRegistration/TouchServerJobTests.cs | 95 ++++++++
.../Jobs/TempFileCleanupJobTests.cs | 50 +++++
...ecurringBackgroundJobHostedServiceTests.cs | 208 ++++++++++++++++++
.../HealthCheckNotifierTests.cs | 1 +
.../HostedServices/KeepAliveTests.cs | 1 +
.../HostedServices/LogScrubberTests.cs | 1 +
.../ScheduledPublishingTests.cs | 1 +
.../InstructionProcessTaskTests.cs | 1 +
.../TouchServerTaskTests.cs | 1 +
.../HostedServices/TempFileCleanupTests.cs | 1 +
50 files changed, 2099 insertions(+), 3 deletions(-)
create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/DelayCalculator.cs
create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/IRecurringBackgroundJob.cs
create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ContentVersionCleanupJob.cs
create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/HealthCheckNotifierJob.cs
create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/KeepAliveJob.cs
create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/LogScrubberJob.cs
create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ReportSiteJob.cs
create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ScheduledPublishingJob.cs
create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/InstructionProcessJob.cs
create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/TouchServerJob.cs
create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/Jobs/TempFileCleanupJob.cs
create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedService.cs
create mode 100644 src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedServiceRunner.cs
create mode 100644 src/Umbraco.Infrastructure/Extensions/ServiceCollectionExtensions.cs
create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobExecutedNotification.cs
create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobExecutingNotification.cs
create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobFailedNotification.cs
create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobIgnoredNotification.cs
create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobNotification.cs
create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStartedNotification.cs
create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStartingNotification.cs
create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStoppedNotification.cs
create mode 100644 src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStoppingNotification.cs
create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/HealthCheckNotifierJobTests.cs
create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/KeepAliveJobTests.cs
create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/LogScrubberJobTests.cs
create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/ScheduledPublishingJobTests.cs
create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/InstructionProcessJobTests.cs
create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/TouchServerJobTests.cs
create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/TempFileCleanupJobTests.cs
create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedServiceTests.cs
diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/DelayCalculator.cs b/src/Umbraco.Infrastructure/BackgroundJobs/DelayCalculator.cs
new file mode 100644
index 0000000000..42d016c066
--- /dev/null
+++ b/src/Umbraco.Infrastructure/BackgroundJobs/DelayCalculator.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Umbraco.Cms.Core.Configuration;
+
+namespace Umbraco.Cms.Infrastructure.BackgroundJobs
+{
+ public class DelayCalculator
+ {
+ ///
+ /// Determines the delay before the first run of a recurring task implemented as a hosted service when an optonal
+ /// configuration for the first run time is available.
+ ///
+ /// The configured time to first run the task in crontab format.
+ /// An instance of
+ /// The logger.
+ /// The default delay to use when a first run time is not configured.
+ /// The delay before first running the recurring task.
+ public static TimeSpan GetDelay(
+ string firstRunTime,
+ ICronTabParser cronTabParser,
+ ILogger logger,
+ TimeSpan defaultDelay) => GetDelay(firstRunTime, cronTabParser, logger, DateTime.Now, defaultDelay);
+
+ ///
+ /// Determines the delay before the first run of a recurring task implemented as a hosted service when an optonal
+ /// configuration for the first run time is available.
+ ///
+ /// The configured time to first run the task in crontab format.
+ /// An instance of
+ /// The logger.
+ /// The current datetime.
+ /// The default delay to use when a first run time is not configured.
+ /// The delay before first running the recurring task.
+ /// Internal to expose for unit tests.
+ internal static TimeSpan GetDelay(
+ string firstRunTime,
+ ICronTabParser cronTabParser,
+ ILogger logger,
+ DateTime now,
+ TimeSpan defaultDelay)
+ {
+ // If first run time not set, start with just small delay after application start.
+ if (string.IsNullOrEmpty(firstRunTime))
+ {
+ return defaultDelay;
+ }
+
+ // If first run time not a valid cron tab, log, and revert to small delay after application start.
+ if (!cronTabParser.IsValidCronTab(firstRunTime))
+ {
+ logger.LogWarning("Could not parse {FirstRunTime} as a crontab expression. Defaulting to default delay for hosted service start.", firstRunTime);
+ return defaultDelay;
+ }
+
+ // Otherwise start at scheduled time according to cron expression, unless within the default delay period.
+ DateTime firstRunOccurance = cronTabParser.GetNextOccurrence(firstRunTime, now);
+ TimeSpan delay = firstRunOccurance - now;
+ return delay < defaultDelay
+ ? defaultDelay
+ : delay;
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/IRecurringBackgroundJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/IRecurringBackgroundJob.cs
new file mode 100644
index 0000000000..c6be3dcec5
--- /dev/null
+++ b/src/Umbraco.Infrastructure/BackgroundJobs/IRecurringBackgroundJob.cs
@@ -0,0 +1,28 @@
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Sync;
+
+namespace Umbraco.Cms.Infrastructure.BackgroundJobs;
+///
+/// A recurring background job
+///
+public interface IRecurringBackgroundJob
+{
+ static readonly TimeSpan DefaultDelay = System.TimeSpan.FromMinutes(3);
+ static readonly ServerRole[] DefaultServerRoles = new[] { ServerRole.Single, ServerRole.SchedulingPublisher };
+
+ /// Timespan representing how often the task should recur.
+ TimeSpan Period { get; }
+
+ ///
+ /// Timespan representing the initial delay after application start-up before the first run of the task
+ /// occurs.
+ ///
+ TimeSpan Delay { get => DefaultDelay; }
+
+ ServerRole[] ServerRoles { get => DefaultServerRoles; }
+
+ event EventHandler PeriodChanged;
+
+ Task RunJobAsync();
+}
+
diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ContentVersionCleanupJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ContentVersionCleanupJob.cs
new file mode 100644
index 0000000000..cb89d600aa
--- /dev/null
+++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ContentVersionCleanupJob.cs
@@ -0,0 +1,66 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Runtime;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+using Umbraco.Cms.Infrastructure.BackgroundJobs;
+
+namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
+
+///
+/// Recurring hosted service that executes the content history cleanup.
+///
+public class ContentVersionCleanupJob : IRecurringBackgroundJob
+{
+
+ public TimeSpan Period { get => TimeSpan.FromHours(1); }
+
+ // No-op event as the period never changes on this job
+ public event EventHandler PeriodChanged { add { } remove { } }
+
+ private readonly ILogger _logger;
+ private readonly IContentVersionService _service;
+ private readonly IOptionsMonitor _settingsMonitor;
+
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ContentVersionCleanupJob(
+ ILogger logger,
+ IOptionsMonitor settingsMonitor,
+ IContentVersionService service)
+ {
+ _logger = logger;
+ _settingsMonitor = settingsMonitor;
+ _service = service;
+ }
+
+ ///
+ public Task RunJobAsync()
+ {
+ // Globally disabled by feature flag
+ if (!_settingsMonitor.CurrentValue.ContentVersionCleanupPolicy.EnableCleanup)
+ {
+ _logger.LogInformation(
+ "ContentVersionCleanup task will not run as it has been globally disabled via configuration");
+ return Task.CompletedTask;
+ }
+
+
+ var count = _service.PerformContentVersionCleanup(DateTime.Now).Count;
+
+ if (count > 0)
+ {
+ _logger.LogInformation("Deleted {count} ContentVersion(s)", count);
+ }
+ else
+ {
+ _logger.LogDebug("Task complete, no items were Deleted");
+ }
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/HealthCheckNotifierJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/HealthCheckNotifierJob.cs
new file mode 100644
index 0000000000..ba78af33b4
--- /dev/null
+++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/HealthCheckNotifierJob.cs
@@ -0,0 +1,116 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Configuration;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.HealthChecks;
+using Umbraco.Cms.Core.HealthChecks.NotificationMethods;
+using Umbraco.Cms.Core.Logging;
+using Umbraco.Cms.Core.Runtime;
+using Umbraco.Cms.Core.Scoping;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
+
+///
+/// Hosted service implementation for recurring health check notifications.
+///
+public class HealthCheckNotifierJob : IRecurringBackgroundJob
+{
+
+
+ public TimeSpan Period { get; private set; }
+ public TimeSpan Delay { get; private set; }
+
+ private event EventHandler? _periodChanged;
+ public event EventHandler PeriodChanged
+ {
+ add { _periodChanged += value; }
+ remove { _periodChanged -= value; }
+ }
+
+ private readonly HealthCheckCollection _healthChecks;
+ private readonly ILogger _logger;
+ private readonly HealthCheckNotificationMethodCollection _notifications;
+ private readonly IProfilingLogger _profilingLogger;
+ private readonly ICoreScopeProvider _scopeProvider;
+ private HealthChecksSettings _healthChecksSettings;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The configuration for health check settings.
+ /// The collection of healthchecks.
+ /// The collection of healthcheck notification methods.
+ /// Provides scopes for database operations.
+ /// The typed logger.
+ /// The profiling logger.
+ /// Parser of crontab expressions.
+ public HealthCheckNotifierJob(
+ IOptionsMonitor healthChecksSettings,
+ HealthCheckCollection healthChecks,
+ HealthCheckNotificationMethodCollection notifications,
+ ICoreScopeProvider scopeProvider,
+ ILogger logger,
+ IProfilingLogger profilingLogger,
+ ICronTabParser cronTabParser)
+ {
+ _healthChecksSettings = healthChecksSettings.CurrentValue;
+ _healthChecks = healthChecks;
+ _notifications = notifications;
+ _scopeProvider = scopeProvider;
+ _logger = logger;
+ _profilingLogger = profilingLogger;
+
+ Period = healthChecksSettings.CurrentValue.Notification.Period;
+ Delay = DelayCalculator.GetDelay(healthChecksSettings.CurrentValue.Notification.FirstRunTime, cronTabParser, logger, TimeSpan.FromMinutes(3));
+
+
+ healthChecksSettings.OnChange(x =>
+ {
+ _healthChecksSettings = x;
+ Period = x.Notification.Period;
+ _periodChanged?.Invoke(this, EventArgs.Empty);
+ });
+ }
+
+ public async Task RunJobAsync()
+ {
+ if (_healthChecksSettings.Notification.Enabled == false)
+ {
+ return;
+ }
+
+ // Ensure we use an explicit scope since we are running on a background thread and plugin health
+ // checks can be making service/database calls so we want to ensure the CallContext/Ambient scope
+ // isn't used since that can be problematic.
+ using (ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true))
+ using (_profilingLogger.DebugDuration("Health checks executing", "Health checks complete"))
+ {
+ // Don't notify for any checks that are disabled, nor for any disabled just for notifications.
+ Guid[] disabledCheckIds = _healthChecksSettings.Notification.DisabledChecks
+ .Select(x => x.Id)
+ .Union(_healthChecksSettings.DisabledChecks
+ .Select(x => x.Id))
+ .Distinct()
+ .ToArray();
+
+ IEnumerable checks = _healthChecks
+ .Where(x => disabledCheckIds.Contains(x.Id) == false);
+
+ HealthCheckResults results = await HealthCheckResults.Create(checks);
+ results.LogResults();
+
+ // Send using registered notification methods that are enabled.
+ foreach (IHealthCheckNotificationMethod notificationMethod in _notifications.Where(x => x.Enabled))
+ {
+ await notificationMethod.SendAsync(results);
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/KeepAliveJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/KeepAliveJob.cs
new file mode 100644
index 0000000000..a9849ddbb7
--- /dev/null
+++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/KeepAliveJob.cs
@@ -0,0 +1,90 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Hosting;
+using Umbraco.Cms.Core.Logging;
+using Umbraco.Cms.Core.Routing;
+using Umbraco.Cms.Core.Runtime;
+using Umbraco.Cms.Core.Sync;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
+
+///
+/// Hosted service implementation for keep alive feature.
+///
+public class KeepAliveJob : IRecurringBackgroundJob
+{
+ public TimeSpan Period { get => TimeSpan.FromMinutes(5); }
+
+ // No-op event as the period never changes on this job
+ public event EventHandler PeriodChanged { add { } remove { } }
+
+ private readonly IHostingEnvironment _hostingEnvironment;
+ private readonly IHttpClientFactory _httpClientFactory;
+ private readonly ILogger _logger;
+ private readonly IProfilingLogger _profilingLogger;
+ private KeepAliveSettings _keepAliveSettings;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The current hosting environment
+ /// The configuration for keep alive settings.
+ /// The typed logger.
+ /// The profiling logger.
+ /// Factory for instances.
+ public KeepAliveJob(
+ IHostingEnvironment hostingEnvironment,
+ IOptionsMonitor keepAliveSettings,
+ ILogger logger,
+ IProfilingLogger profilingLogger,
+ IHttpClientFactory httpClientFactory)
+ {
+ _hostingEnvironment = hostingEnvironment;
+ _keepAliveSettings = keepAliveSettings.CurrentValue;
+ _logger = logger;
+ _profilingLogger = profilingLogger;
+ _httpClientFactory = httpClientFactory;
+
+ keepAliveSettings.OnChange(x => _keepAliveSettings = x);
+ }
+
+ public async Task RunJobAsync()
+ {
+ if (_keepAliveSettings.DisableKeepAliveTask)
+ {
+ return;
+ }
+
+ using (_profilingLogger.DebugDuration("Keep alive executing", "Keep alive complete"))
+ {
+ var umbracoAppUrl = _hostingEnvironment.ApplicationMainUrl?.ToString();
+ if (umbracoAppUrl.IsNullOrWhiteSpace())
+ {
+ _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip.");
+ return;
+ }
+
+ // If the config is an absolute path, just use it
+ var keepAlivePingUrl = WebPath.Combine(
+ umbracoAppUrl!,
+ _hostingEnvironment.ToAbsolute(_keepAliveSettings.KeepAlivePingUrl));
+
+ try
+ {
+ var request = new HttpRequestMessage(HttpMethod.Get, keepAlivePingUrl);
+ HttpClient httpClient = _httpClientFactory.CreateClient(Constants.HttpClients.IgnoreCertificateErrors);
+ _ = await httpClient.SendAsync(request);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Keep alive failed (at '{keepAlivePingUrl}').", keepAlivePingUrl);
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/LogScrubberJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/LogScrubberJob.cs
new file mode 100644
index 0000000000..1c745661cb
--- /dev/null
+++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/LogScrubberJob.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Logging;
+using Umbraco.Cms.Core.Runtime;
+using Umbraco.Cms.Core.Scoping;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+
+namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
+
+///
+/// Log scrubbing hosted service.
+///
+///
+/// Will only run on non-replica servers.
+///
+public class LogScrubberJob : IRecurringBackgroundJob
+{
+ public TimeSpan Period { get => TimeSpan.FromHours(4); }
+
+ // No-op event as the period never changes on this job
+ public event EventHandler PeriodChanged { add { } remove { } }
+
+
+ private readonly IAuditService _auditService;
+ private readonly ILogger _logger;
+ private readonly IProfilingLogger _profilingLogger;
+ private readonly ICoreScopeProvider _scopeProvider;
+ private LoggingSettings _settings;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Service for handling audit operations.
+ /// The configuration for logging settings.
+ /// Provides scopes for database operations.
+ /// The typed logger.
+ /// The profiling logger.
+ public LogScrubberJob(
+ IAuditService auditService,
+ IOptionsMonitor settings,
+ ICoreScopeProvider scopeProvider,
+ ILogger logger,
+ IProfilingLogger profilingLogger)
+ {
+
+ _auditService = auditService;
+ _settings = settings.CurrentValue;
+ _scopeProvider = scopeProvider;
+ _logger = logger;
+ _profilingLogger = profilingLogger;
+ settings.OnChange(x => _settings = x);
+ }
+
+ public Task RunJobAsync()
+ {
+
+ // Ensure we use an explicit scope since we are running on a background thread.
+ using (ICoreScope scope = _scopeProvider.CreateCoreScope())
+ using (_profilingLogger.DebugDuration("Log scrubbing executing", "Log scrubbing complete"))
+ {
+ _auditService.CleanLogs((int)_settings.MaxLogAge.TotalMinutes);
+ _ = scope.Complete();
+ }
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ReportSiteJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ReportSiteJob.cs
new file mode 100644
index 0000000000..5d39b57add
--- /dev/null
+++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ReportSiteJob.cs
@@ -0,0 +1,92 @@
+using System.Text;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Newtonsoft.Json;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Configuration;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.DependencyInjection;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+using Umbraco.Cms.Core.Telemetry;
+using Umbraco.Cms.Core.Telemetry.Models;
+
+namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
+
+public class ReportSiteJob : IRecurringBackgroundJob
+{
+
+ public TimeSpan Period { get => TimeSpan.FromDays(1); }
+ public TimeSpan Delay { get => TimeSpan.FromMinutes(5); }
+ public ServerRole[] ServerRoles { get => Enum.GetValues(); }
+
+ // No-op event as the period never changes on this job
+ public event EventHandler PeriodChanged { add { } remove { } }
+
+
+ private static HttpClient _httpClient = new();
+ private readonly ILogger _logger;
+ private readonly ITelemetryService _telemetryService;
+
+
+ public ReportSiteJob(
+ ILogger logger,
+ ITelemetryService telemetryService)
+ {
+ _logger = logger;
+ _telemetryService = telemetryService;
+ _httpClient = new HttpClient();
+ }
+
+ ///
+ /// Runs the background task to send the anonymous ID
+ /// to telemetry service
+ ///
+ public async Task RunJobAsync()
+ {
+
+ if (_telemetryService.TryGetTelemetryReportData(out TelemetryReportData? telemetryReportData) is false)
+ {
+ _logger.LogWarning("No telemetry marker found");
+
+ return;
+ }
+
+ try
+ {
+ if (_httpClient.BaseAddress is null)
+ {
+ // Send data to LIVE telemetry
+ _httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/");
+
+#if DEBUG
+ // Send data to DEBUG telemetry service
+ _httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/");
+#endif
+ }
+
+ _httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");
+
+ using (var request = new HttpRequestMessage(HttpMethod.Post, "installs/"))
+ {
+ request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8,
+ "application/json");
+
+ // Make a HTTP Post to telemetry service
+ // https://telemetry.umbraco.com/installs/
+ // Fire & Forget, do not need to know if its a 200, 500 etc
+ using (await _httpClient.SendAsync(request))
+ {
+ }
+ }
+ }
+ catch
+ {
+ // Silently swallow
+ // The user does not need the logs being polluted if our service has fallen over or is down etc
+ // Hence only logging this at a more verbose level (which users should not be using in production)
+ _logger.LogDebug("There was a problem sending a request to the Umbraco telemetry service");
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ScheduledPublishingJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ScheduledPublishingJob.cs
new file mode 100644
index 0000000000..f815366a21
--- /dev/null
+++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ScheduledPublishingJob.cs
@@ -0,0 +1,105 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Microsoft.Extensions.Logging;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Runtime;
+using Umbraco.Cms.Core.Scoping;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+using Umbraco.Cms.Core.Web;
+
+namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
+
+///
+/// Hosted service implementation for scheduled publishing feature.
+///
+///
+/// Runs only on non-replica servers.
+///
+public class ScheduledPublishingJob : IRecurringBackgroundJob
+{
+ public TimeSpan Period { get => TimeSpan.FromMinutes(1); }
+ // No-op event as the period never changes on this job
+ public event EventHandler PeriodChanged { add { } remove { } }
+
+
+ private readonly IContentService _contentService;
+ private readonly ILogger _logger;
+ private readonly ICoreScopeProvider _scopeProvider;
+ private readonly IServerMessenger _serverMessenger;
+ private readonly IUmbracoContextFactory _umbracoContextFactory;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ScheduledPublishingJob(
+ IContentService contentService,
+ IUmbracoContextFactory umbracoContextFactory,
+ ILogger logger,
+ IServerMessenger serverMessenger,
+ ICoreScopeProvider scopeProvider)
+ {
+ _contentService = contentService;
+ _umbracoContextFactory = umbracoContextFactory;
+ _logger = logger;
+ _serverMessenger = serverMessenger;
+ _scopeProvider = scopeProvider;
+ }
+
+ public Task RunJobAsync()
+ {
+ if (Suspendable.ScheduledPublishing.CanRun == false)
+ {
+ return Task.CompletedTask;
+ }
+
+ try
+ {
+ // Ensure we run with an UmbracoContext, because this will run in a background task,
+ // and developers may be using the UmbracoContext in the event handlers.
+
+ // TODO: or maybe not, CacheRefresherComponent already ensures a context when handling events
+ // - UmbracoContext 'current' needs to be refactored and cleaned up
+ // - batched messenger should not depend on a current HttpContext
+ // but then what should be its "scope"? could we attach it to scopes?
+ // - and we should definitively *not* have to flush it here (should be auto)
+ using UmbracoContextReference contextReference = _umbracoContextFactory.EnsureUmbracoContext();
+ using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
+
+ /* We used to assume that there will never be two instances running concurrently where (IsMainDom && ServerRole == SchedulingPublisher)
+ * However this is possible during an azure deployment slot swap for the SchedulingPublisher instance when trying to achieve zero downtime deployments.
+ * If we take a distributed write lock, we are certain that the multiple instances of the job will not run in parallel.
+ * It's possible that during the swapping process we may run this job more frequently than intended but this is not of great concern and it's
+ * only until the old SchedulingPublisher shuts down. */
+ scope.EagerWriteLock(Constants.Locks.ScheduledPublishing);
+ try
+ {
+ // Run
+ IEnumerable result = _contentService.PerformScheduledPublish(DateTime.Now);
+ foreach (IGrouping grouped in result.GroupBy(x => x.Result))
+ {
+ _logger.LogInformation(
+ "Scheduled publishing result: '{StatusCount}' items with status {Status}",
+ grouped.Count(),
+ grouped.Key);
+ }
+ }
+ finally
+ {
+ // If running on a temp context, we have to flush the messenger
+ if (contextReference.IsRoot)
+ {
+ _serverMessenger.SendMessages();
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ // important to catch *everything* to ensure the task repeats
+ _logger.LogError(ex, "Failed.");
+ }
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/InstructionProcessJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/InstructionProcessJob.cs
new file mode 100644
index 0000000000..cc6e35bf83
--- /dev/null
+++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/InstructionProcessJob.cs
@@ -0,0 +1,59 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+
+namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs.ServerRegistration;
+
+///
+/// Implements periodic database instruction processing as a hosted service.
+///
+public class InstructionProcessJob : IRecurringBackgroundJob
+{
+
+ public TimeSpan Period { get; }
+ public TimeSpan Delay { get => TimeSpan.FromMinutes(1); }
+ public ServerRole[] ServerRoles { get => Enum.GetValues(); }
+
+ // No-op event as the period never changes on this job
+ public event EventHandler PeriodChanged { add { } remove { } }
+
+ private readonly ILogger _logger;
+ private readonly IServerMessenger _messenger;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Service broadcasting cache notifications to registered servers.
+ /// The typed logger.
+ /// The configuration for global settings.
+ public InstructionProcessJob(
+ IServerMessenger messenger,
+ ILogger logger,
+ IOptions globalSettings)
+ {
+ _messenger = messenger;
+ _logger = logger;
+
+ Period = globalSettings.Value.DatabaseServerMessenger.TimeBetweenSyncOperations;
+ }
+
+ public Task RunJobAsync()
+ {
+ try
+ {
+ _messenger.Sync();
+ }
+ catch (Exception e)
+ {
+ _logger.LogError(e, "Failed (will repeat).");
+ }
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/TouchServerJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/TouchServerJob.cs
new file mode 100644
index 0000000000..8258da1a35
--- /dev/null
+++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/ServerRegistration/TouchServerJob.cs
@@ -0,0 +1,102 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Hosting;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs.ServerRegistration;
+
+///
+/// Implements periodic server "touching" (to mark as active/deactive) as a hosted service.
+///
+public class TouchServerJob : IRecurringBackgroundJob
+{
+ public TimeSpan Period { get; private set; }
+ public TimeSpan Delay { get => TimeSpan.FromSeconds(15); }
+
+ // Runs on all servers
+ public ServerRole[] ServerRoles { get => Enum.GetValues(); }
+
+ private event EventHandler? _periodChanged;
+ public event EventHandler PeriodChanged {
+ add { _periodChanged += value; }
+ remove { _periodChanged -= value; }
+ }
+
+
+ private readonly IHostingEnvironment _hostingEnvironment;
+ private readonly ILogger _logger;
+ private readonly IServerRegistrationService _serverRegistrationService;
+ private readonly IServerRoleAccessor _serverRoleAccessor;
+ private GlobalSettings _globalSettings;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Services for server registrations.
+ /// The typed logger.
+ /// The configuration for global settings.
+ /// The hostingEnviroment.
+ /// The accessor for the server role
+ public TouchServerJob(
+ IServerRegistrationService serverRegistrationService,
+ IHostingEnvironment hostingEnvironment,
+ ILogger logger,
+ IOptionsMonitor globalSettings,
+ IServerRoleAccessor serverRoleAccessor)
+ {
+ _serverRegistrationService = serverRegistrationService ??
+ throw new ArgumentNullException(nameof(serverRegistrationService));
+ _hostingEnvironment = hostingEnvironment;
+ _logger = logger;
+ _globalSettings = globalSettings.CurrentValue;
+ _serverRoleAccessor = serverRoleAccessor;
+
+ Period = _globalSettings.DatabaseServerRegistrar.WaitTimeBetweenCalls;
+ globalSettings.OnChange(x =>
+ {
+ _globalSettings = x;
+ Period = x.DatabaseServerRegistrar.WaitTimeBetweenCalls;
+
+ _periodChanged?.Invoke(this, EventArgs.Empty);
+ });
+ }
+
+ public Task RunJobAsync()
+ {
+
+ // If the IServerRoleAccessor has been changed away from ElectedServerRoleAccessor this task no longer makes sense,
+ // since all it's used for is to allow the ElectedServerRoleAccessor
+ // to figure out what role a given server has, so we just stop this task.
+ if (_serverRoleAccessor is not ElectedServerRoleAccessor)
+ {
+ return Task.CompletedTask;
+ }
+
+ var serverAddress = _hostingEnvironment.ApplicationMainUrl?.ToString();
+ if (serverAddress.IsNullOrWhiteSpace())
+ {
+ _logger.LogWarning("No umbracoApplicationUrl for service (yet), skip.");
+ return Task.CompletedTask;
+ }
+
+ try
+ {
+ _serverRegistrationService.TouchServer(
+ serverAddress!,
+ _globalSettings.DatabaseServerRegistrar.StaleServerTimeout);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to update server record in database.");
+ }
+
+ return Task.CompletedTask;
+ }
+}
diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/TempFileCleanupJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/TempFileCleanupJob.cs
new file mode 100644
index 0000000000..ac88a8edce
--- /dev/null
+++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/TempFileCleanupJob.cs
@@ -0,0 +1,99 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using Microsoft.Extensions.Logging;
+using Umbraco.Cms.Core.IO;
+using Umbraco.Cms.Core.Runtime;
+using Umbraco.Cms.Core.Sync;
+
+namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
+
+///
+/// Used to cleanup temporary file locations.
+///
+///
+/// Will run on all servers - even though file upload should only be handled on the scheduling publisher, this will
+/// ensure that in the case it happens on subscribers that they are cleaned up too.
+///
+public class TempFileCleanupJob : IRecurringBackgroundJob
+{
+ public TimeSpan Period { get => TimeSpan.FromMinutes(60); }
+
+ // Runs on all servers
+ public ServerRole[] ServerRoles { get => Enum.GetValues(); }
+
+ // No-op event as the period never changes on this job
+ public event EventHandler PeriodChanged { add { } remove { } }
+
+ private readonly TimeSpan _age = TimeSpan.FromDays(1);
+ private readonly IIOHelper _ioHelper;
+ private readonly ILogger _logger;
+ private readonly DirectoryInfo[] _tempFolders;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Helper service for IO operations.
+ /// The typed logger.
+ public TempFileCleanupJob(IIOHelper ioHelper, ILogger logger)
+ {
+ _ioHelper = ioHelper;
+ _logger = logger;
+
+ _tempFolders = _ioHelper.GetTempFolders();
+ }
+
+ public Task RunJobAsync()
+ {
+ foreach (DirectoryInfo folder in _tempFolders)
+ {
+ CleanupFolder(folder);
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private void CleanupFolder(DirectoryInfo folder)
+ {
+ CleanFolderResult result = _ioHelper.CleanFolder(folder, _age);
+ switch (result.Status)
+ {
+ case CleanFolderResultStatus.FailedAsDoesNotExist:
+ _logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName);
+ break;
+ case CleanFolderResultStatus.FailedWithException:
+ foreach (CleanFolderResult.Error error in result.Errors!)
+ {
+ _logger.LogError(error.Exception, "Could not delete temp file {FileName}",
+ error.ErroringFile.FullName);
+ }
+
+ break;
+ }
+
+ folder.Refresh(); // In case it's changed during runtime
+ if (!folder.Exists)
+ {
+ _logger.LogDebug("The cleanup folder doesn't exist {Folder}", folder.FullName);
+ return;
+ }
+
+ FileInfo[] files = folder.GetFiles("*.*", SearchOption.AllDirectories);
+ foreach (FileInfo file in files)
+ {
+ if (DateTime.UtcNow - file.LastWriteTimeUtc > _age)
+ {
+ try
+ {
+ file.IsReadOnly = false;
+ file.Delete();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Could not delete temp file {FileName}", file.FullName);
+ }
+ }
+ }
+ }
+
+}
diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedService.cs b/src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedService.cs
new file mode 100644
index 0000000000..80afdb903c
--- /dev/null
+++ b/src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedService.cs
@@ -0,0 +1,127 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Serilog.Core;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Runtime;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+using Umbraco.Cms.Infrastructure.HostedServices;
+
+namespace Umbraco.Cms.Infrastructure.BackgroundJobs;
+
+public static class RecurringBackgroundJobHostedService
+{
+ public static Func CreateHostedServiceFactory(IServiceProvider serviceProvider) =>
+ (IRecurringBackgroundJob job) =>
+ {
+ Type hostedServiceType = typeof(RecurringBackgroundJobHostedService<>).MakeGenericType(job.GetType());
+ return (IHostedService)ActivatorUtilities.CreateInstance(serviceProvider, hostedServiceType, job);
+ };
+}
+
+///
+/// Runs a recurring background job inside a hosted service.
+/// Generic version for DependencyInjection
+///
+/// Type of the Job
+public class RecurringBackgroundJobHostedService : RecurringHostedServiceBase where TJob : IRecurringBackgroundJob
+{
+
+ private readonly ILogger> _logger;
+ private readonly IMainDom _mainDom;
+ private readonly IRuntimeState _runtimeState;
+ private readonly IServerRoleAccessor _serverRoleAccessor;
+ private readonly IEventAggregator _eventAggregator;
+ private readonly IRecurringBackgroundJob _job;
+
+ public RecurringBackgroundJobHostedService(
+ IRuntimeState runtimeState,
+ ILogger> logger,
+ IMainDom mainDom,
+ IServerRoleAccessor serverRoleAccessor,
+ IEventAggregator eventAggregator,
+ TJob job)
+ : base(logger, job.Period, job.Delay)
+ {
+ _runtimeState = runtimeState;
+ _logger = logger;
+ _mainDom = mainDom;
+ _serverRoleAccessor = serverRoleAccessor;
+ _eventAggregator = eventAggregator;
+ _job = job;
+
+ _job.PeriodChanged += (sender, e) => ChangePeriod(_job.Period);
+ }
+
+ ///
+ public override async Task PerformExecuteAsync(object? state)
+ {
+ var executingNotification = new Notifications.RecurringBackgroundJobExecutingNotification(_job, new EventMessages());
+ await _eventAggregator.PublishAsync(executingNotification);
+
+ try
+ {
+
+ if (_runtimeState.Level != RuntimeLevel.Run)
+ {
+ _logger.LogDebug("Job not running as runlevel not yet ready");
+ await _eventAggregator.PublishAsync(new Notifications.RecurringBackgroundJobIgnoredNotification(_job, new EventMessages()).WithStateFrom(executingNotification));
+ return;
+ }
+
+ // Don't run on replicas nor unknown role servers
+ if (!_job.ServerRoles.Contains(_serverRoleAccessor.CurrentServerRole))
+ {
+ _logger.LogDebug("Job not running on this server role");
+ await _eventAggregator.PublishAsync(new Notifications.RecurringBackgroundJobIgnoredNotification(_job, new EventMessages()).WithStateFrom(executingNotification));
+ return;
+ }
+
+ // Ensure we do not run if not main domain, but do NOT lock it
+ if (!_mainDom.IsMainDom)
+ {
+ _logger.LogDebug("Job not running as not MainDom");
+ await _eventAggregator.PublishAsync(new Notifications.RecurringBackgroundJobIgnoredNotification(_job, new EventMessages()).WithStateFrom(executingNotification));
+ return;
+ }
+
+
+ await _job.RunJobAsync();
+ await _eventAggregator.PublishAsync(new Notifications.RecurringBackgroundJobExecutedNotification(_job, new EventMessages()).WithStateFrom(executingNotification));
+
+
+ }
+ catch (Exception ex)
+ {
+ await _eventAggregator.PublishAsync(new Notifications.RecurringBackgroundJobFailedNotification(_job, new EventMessages()).WithStateFrom(executingNotification));
+ _logger.LogError(ex, "Unhandled exception in recurring background job.");
+ }
+
+ }
+
+ public override async Task StartAsync(CancellationToken cancellationToken)
+ {
+ var startingNotification = new Notifications.RecurringBackgroundJobStartingNotification(_job, new EventMessages());
+ await _eventAggregator.PublishAsync(startingNotification);
+
+ await base.StartAsync(cancellationToken);
+
+ await _eventAggregator.PublishAsync(new Notifications.RecurringBackgroundJobStartedNotification(_job, new EventMessages()).WithStateFrom(startingNotification));
+
+ }
+
+ public override async Task StopAsync(CancellationToken cancellationToken)
+ {
+ var stoppingNotification = new Notifications.RecurringBackgroundJobStoppingNotification(_job, new EventMessages());
+ await _eventAggregator.PublishAsync(stoppingNotification);
+
+ await base.StopAsync(cancellationToken);
+
+ await _eventAggregator.PublishAsync(new Notifications.RecurringBackgroundJobStoppedNotification(_job, new EventMessages()).WithStateFrom(stoppingNotification));
+ }
+}
diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedServiceRunner.cs b/src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedServiceRunner.cs
new file mode 100644
index 0000000000..0e56bfa2b1
--- /dev/null
+++ b/src/Umbraco.Infrastructure/BackgroundJobs/RecurringBackgroundJobHostedServiceRunner.cs
@@ -0,0 +1,81 @@
+using System.Linq;
+using System.Threading;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Umbraco.Cms.Core.Runtime;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+using Umbraco.Cms.Infrastructure.ModelsBuilder;
+
+namespace Umbraco.Cms.Infrastructure.BackgroundJobs;
+
+///
+/// A hosted service that discovers and starts hosted services for any recurring background jobs in the DI container.
+///
+public class RecurringBackgroundJobHostedServiceRunner : IHostedService
+{
+ private readonly ILogger _logger;
+ private readonly List _jobs;
+ private readonly Func _jobFactory;
+ private IList _hostedServices = new List();
+
+
+ public RecurringBackgroundJobHostedServiceRunner(
+ ILogger logger,
+ IEnumerable jobs,
+ Func jobFactory)
+ {
+ _jobs = jobs.ToList();
+ _logger = logger;
+ _jobFactory = jobFactory;
+ }
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ _logger.LogInformation("Creating recurring background jobs hosted services");
+
+ // create hosted services for each background job
+ _hostedServices = _jobs.Select(_jobFactory).ToList();
+
+ _logger.LogInformation("Starting recurring background jobs hosted services");
+
+ foreach (IHostedService hostedService in _hostedServices)
+ {
+ try
+ {
+ _logger.LogInformation($"Starting background hosted service for {hostedService.GetType().Name}");
+ await hostedService.StartAsync(cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception exception)
+ {
+ _logger.LogError(exception, $"Failed to start background hosted service for {hostedService.GetType().Name}");
+ }
+ }
+
+ _logger.LogInformation("Completed starting recurring background jobs hosted services");
+
+
+ }
+
+ public async Task StopAsync(CancellationToken stoppingToken)
+ {
+ _logger.LogInformation("Stopping recurring background jobs hosted services");
+
+ foreach (IHostedService hostedService in _hostedServices)
+ {
+ try
+ {
+ _logger.LogInformation($"Stopping background hosted service for {hostedService.GetType().Name}");
+ await hostedService.StopAsync(stoppingToken).ConfigureAwait(false);
+ }
+ catch (Exception exception)
+ {
+ _logger.LogError(exception, $"Failed to stop background hosted service for {hostedService.GetType().Name}");
+ }
+ }
+
+ _logger.LogInformation("Completed stopping recurring background jobs hosted services");
+
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Extensions/ServiceCollectionExtensions.cs b/src/Umbraco.Infrastructure/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000000..efaac24ceb
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,31 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Umbraco.Cms.Core.Composing;
+using Umbraco.Cms.Infrastructure.BackgroundJobs;
+
+namespace Umbraco.Extensions;
+
+public static class ServiceCollectionExtensions
+{
+ ///
+ /// Adds a recurring background job with an implementation type of
+ /// to the specified .
+ ///
+ public static void AddRecurringBackgroundJob(
+ this IServiceCollection services)
+ where TJob : class, IRecurringBackgroundJob =>
+ services.AddSingleton();
+
+ ///
+ /// Adds a recurring background job with an implementation type of
+ /// using the factory
+ /// to the specified .
+ ///
+ public static void AddRecurringBackgroundJob(
+ this IServiceCollection services,
+ Func implementationFactory)
+ where TJob : class, IRecurringBackgroundJob =>
+ services.AddSingleton(implementationFactory);
+
+}
+
diff --git a/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs
index 37eeb668f9..1ecfcbf926 100644
--- a/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/ContentVersionCleanup.cs
@@ -11,6 +11,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices;
///
/// Recurring hosted service that executes the content history cleanup.
///
+[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.ContentVersionCleanupJob instead. This class will be removed in Umbraco 14.")]
public class ContentVersionCleanup : RecurringHostedServiceBase
{
private readonly ILogger _logger;
diff --git a/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs
index e1a10d9f71..5ee76d1a18 100644
--- a/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/HealthCheckNotifier.cs
@@ -20,6 +20,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices;
///
/// Hosted service implementation for recurring health check notifications.
///
+[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.HealthCheckNotifierJob instead. This class will be removed in Umbraco 14.")]
public class HealthCheckNotifier : RecurringHostedServiceBase
{
private readonly HealthCheckCollection _healthChecks;
diff --git a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs
index 5db59ff225..978ffa2dd1 100644
--- a/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/KeepAlive.cs
@@ -17,6 +17,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices;
///
/// Hosted service implementation for keep alive feature.
///
+[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.KeepAliveJob instead. This class will be removed in Umbraco 14.")]
public class KeepAlive : RecurringHostedServiceBase
{
private readonly IHostingEnvironment _hostingEnvironment;
diff --git a/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs b/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs
index 9ae0dfe656..4c3df658c6 100644
--- a/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/LogScrubber.cs
@@ -18,6 +18,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices;
///
/// Will only run on non-replica servers.
///
+[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.LogScrubberJob instead. This class will be removed in Umbraco 14.")]
public class LogScrubber : RecurringHostedServiceBase
{
private readonly IAuditService _auditService;
diff --git a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs
index c100da0ab2..a35f7aa956 100644
--- a/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/RecurringHostedServiceBase.cs
@@ -107,7 +107,7 @@ public abstract class RecurringHostedServiceBase : IHostedService, IDisposable
}
///
- public Task StartAsync(CancellationToken cancellationToken)
+ public virtual Task StartAsync(CancellationToken cancellationToken)
{
using (!ExecutionContext.IsFlowSuppressed() ? (IDisposable)ExecutionContext.SuppressFlow() : null)
{
@@ -118,7 +118,7 @@ public abstract class RecurringHostedServiceBase : IHostedService, IDisposable
}
///
- public Task StopAsync(CancellationToken cancellationToken)
+ public virtual Task StopAsync(CancellationToken cancellationToken)
{
_period = Timeout.InfiniteTimeSpan;
_timer?.Change(Timeout.Infinite, 0);
diff --git a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs
index 7184aaf16e..77800ae7d6 100644
--- a/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/ReportSiteTask.cs
@@ -13,6 +13,7 @@ using Umbraco.Cms.Core.Telemetry.Models;
namespace Umbraco.Cms.Infrastructure.HostedServices;
+[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.ReportSiteJob instead. This class will be removed in Umbraco 14.")]
public class ReportSiteTask : RecurringHostedServiceBase
{
private static HttpClient _httpClient = new();
diff --git a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs
index da1fbaf157..efbd8017df 100644
--- a/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/ScheduledPublishing.cs
@@ -17,6 +17,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices;
///
/// Runs only on non-replica servers.
///
+[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.ScheduledPublishingJob instead. This class will be removed in Umbraco 14.")]
public class ScheduledPublishing : RecurringHostedServiceBase
{
private readonly IContentService _contentService;
diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs
index e4e5700496..fbbdab8878 100644
--- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/InstructionProcessTask.cs
@@ -13,6 +13,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration;
///
/// Implements periodic database instruction processing as a hosted service.
///
+[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.ServerRegistration.InstructionProcessJob instead. This class will be removed in Umbraco 14.")]
public class InstructionProcessTask : RecurringHostedServiceBase
{
private readonly ILogger _logger;
diff --git a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs
index 730282c6b0..a844c33ad6 100644
--- a/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/ServerRegistration/TouchServerTask.cs
@@ -15,6 +15,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration;
///
/// Implements periodic server "touching" (to mark as active/deactive) as a hosted service.
///
+[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.ServerRegistration.TouchServerJob instead. This class will be removed in Umbraco 14.")]
public class TouchServerTask : RecurringHostedServiceBase
{
private readonly IHostingEnvironment _hostingEnvironment;
diff --git a/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs
index cf46e38750..81de651e79 100644
--- a/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs
+++ b/src/Umbraco.Infrastructure/HostedServices/TempFileCleanup.cs
@@ -14,6 +14,7 @@ namespace Umbraco.Cms.Infrastructure.HostedServices;
/// Will run on all servers - even though file upload should only be handled on the scheduling publisher, this will
/// ensure that in the case it happens on subscribers that they are cleaned up too.
///
+[Obsolete("Use Umbraco.Cms.Infrastructure.BackgroundJobs.TempFileCleanupJob instead. This class will be removed in Umbraco 14.")]
public class TempFileCleanup : RecurringHostedServiceBase
{
private readonly TimeSpan _age = TimeSpan.FromDays(1);
diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobExecutedNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobExecutedNotification.cs
new file mode 100644
index 0000000000..8d2fbf96aa
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobExecutedNotification.cs
@@ -0,0 +1,12 @@
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Infrastructure.BackgroundJobs;
+
+namespace Umbraco.Cms.Infrastructure.Notifications
+{
+ public sealed class RecurringBackgroundJobExecutedNotification : RecurringBackgroundJobNotification
+ {
+ public RecurringBackgroundJobExecutedNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages)
+ {
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobExecutingNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobExecutingNotification.cs
new file mode 100644
index 0000000000..71f5cf3edc
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobExecutingNotification.cs
@@ -0,0 +1,12 @@
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Infrastructure.BackgroundJobs;
+
+namespace Umbraco.Cms.Infrastructure.Notifications
+{
+ public sealed class RecurringBackgroundJobExecutingNotification : RecurringBackgroundJobNotification
+ {
+ public RecurringBackgroundJobExecutingNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages)
+ {
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobFailedNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobFailedNotification.cs
new file mode 100644
index 0000000000..594f01fc7b
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobFailedNotification.cs
@@ -0,0 +1,12 @@
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Infrastructure.BackgroundJobs;
+
+namespace Umbraco.Cms.Infrastructure.Notifications
+{
+ public sealed class RecurringBackgroundJobFailedNotification : RecurringBackgroundJobNotification
+ {
+ public RecurringBackgroundJobFailedNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages)
+ {
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobIgnoredNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobIgnoredNotification.cs
new file mode 100644
index 0000000000..8c3a0079d7
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobIgnoredNotification.cs
@@ -0,0 +1,12 @@
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Infrastructure.BackgroundJobs;
+
+namespace Umbraco.Cms.Infrastructure.Notifications
+{
+ public sealed class RecurringBackgroundJobIgnoredNotification : RecurringBackgroundJobNotification
+ {
+ public RecurringBackgroundJobIgnoredNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages)
+ {
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobNotification.cs
new file mode 100644
index 0000000000..f9185cb412
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobNotification.cs
@@ -0,0 +1,12 @@
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Infrastructure.BackgroundJobs;
+
+namespace Umbraco.Cms.Infrastructure.Notifications
+{
+ public class RecurringBackgroundJobNotification : ObjectNotification
+ {
+ public IRecurringBackgroundJob Job { get; }
+ public RecurringBackgroundJobNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages) => Job = target;
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStartedNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStartedNotification.cs
new file mode 100644
index 0000000000..dca1e69d40
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStartedNotification.cs
@@ -0,0 +1,12 @@
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Infrastructure.BackgroundJobs;
+
+namespace Umbraco.Cms.Infrastructure.Notifications
+{
+ public sealed class RecurringBackgroundJobStartedNotification : RecurringBackgroundJobNotification
+ {
+ public RecurringBackgroundJobStartedNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages)
+ {
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStartingNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStartingNotification.cs
new file mode 100644
index 0000000000..3ee8d2a710
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStartingNotification.cs
@@ -0,0 +1,12 @@
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Infrastructure.BackgroundJobs;
+
+namespace Umbraco.Cms.Infrastructure.Notifications
+{
+ public sealed class RecurringBackgroundJobStartingNotification : RecurringBackgroundJobNotification
+ {
+ public RecurringBackgroundJobStartingNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages)
+ {
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStoppedNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStoppedNotification.cs
new file mode 100644
index 0000000000..a1df71a4ee
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStoppedNotification.cs
@@ -0,0 +1,12 @@
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Infrastructure.BackgroundJobs;
+
+namespace Umbraco.Cms.Infrastructure.Notifications
+{
+ public sealed class RecurringBackgroundJobStoppedNotification : RecurringBackgroundJobNotification
+ {
+ public RecurringBackgroundJobStoppedNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages)
+ {
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStoppingNotification.cs b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStoppingNotification.cs
new file mode 100644
index 0000000000..985a20e286
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Notifications/RecurringBackgroundJobStoppingNotification.cs
@@ -0,0 +1,12 @@
+using Umbraco.Cms.Core.Events;
+using Umbraco.Cms.Infrastructure.BackgroundJobs;
+
+namespace Umbraco.Cms.Infrastructure.Notifications
+{
+ public sealed class RecurringBackgroundJobStoppingNotification : RecurringBackgroundJobNotification
+ {
+ public RecurringBackgroundJobStoppingNotification(IRecurringBackgroundJob target, EventMessages messages) : base(target, messages)
+ {
+ }
+ }
+}
diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs
index dfa22b06e8..7c23ce19da 100644
--- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -46,7 +46,7 @@ public static partial class UmbracoBuilderExtensions
.AddMvcAndRazor(configureMvc)
.AddWebServer()
.AddPreviewSupport()
- .AddHostedServices()
+ .AddRecurringBackgroundJobs()
.AddNuCache()
.AddDistributedCache()
.TryAddModelsBuilderDashboard()
diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs
index 2dd828f9c2..71704f4edc 100644
--- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Serilog.Extensions.Logging;
@@ -38,6 +39,9 @@ using Umbraco.Cms.Core.Telemetry;
using Umbraco.Cms.Core.Templates;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Core.WebAssets;
+using Umbraco.Cms.Infrastructure.BackgroundJobs;
+using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
+using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs.ServerRegistration;
using Umbraco.Cms.Infrastructure.DependencyInjection;
using Umbraco.Cms.Infrastructure.HostedServices;
using Umbraco.Cms.Infrastructure.HostedServices.ServerRegistration;
@@ -176,6 +180,7 @@ public static partial class UmbracoBuilderExtensions
///
/// Add Umbraco hosted services
///
+ [Obsolete("Use AddRecurringBackgroundJobs instead")]
public static IUmbracoBuilder AddHostedServices(this IUmbracoBuilder builder)
{
builder.Services.AddHostedService();
@@ -191,6 +196,36 @@ public static partial class UmbracoBuilderExtensions
new ReportSiteTask(
provider.GetRequiredService>(),
provider.GetRequiredService()));
+
+
+ return builder;
+ }
+
+ ///
+ /// Add Umbraco recurring background jobs
+ ///
+ public static IUmbracoBuilder AddRecurringBackgroundJobs(this IUmbracoBuilder builder)
+ {
+ // Add background jobs
+ builder.Services.AddRecurringBackgroundJob();
+ builder.Services.AddRecurringBackgroundJob();
+ builder.Services.AddRecurringBackgroundJob();
+ builder.Services.AddRecurringBackgroundJob();
+ builder.Services.AddRecurringBackgroundJob();
+ builder.Services.AddRecurringBackgroundJob();
+ builder.Services.AddRecurringBackgroundJob();
+ builder.Services.AddRecurringBackgroundJob();
+ builder.Services.AddRecurringBackgroundJob(provider =>
+ new ReportSiteJob(
+ provider.GetRequiredService>(),
+ provider.GetRequiredService()));
+
+
+ builder.Services.AddHostedService();
+ builder.Services.AddSingleton(RecurringBackgroundJobHostedService.CreateHostedServiceFactory);
+ builder.Services.AddHostedService();
+
+
return builder;
}
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/HealthCheckNotifierJobTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/HealthCheckNotifierJobTests.cs
new file mode 100644
index 0000000000..cf9883603b
--- /dev/null
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/HealthCheckNotifierJobTests.cs
@@ -0,0 +1,139 @@
+// Copyright (c) Umbraco.
+// See LICENSE for more details.
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Moq;
+using NUnit.Framework;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.Configuration;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.HealthChecks;
+using Umbraco.Cms.Core.HealthChecks.NotificationMethods;
+using Umbraco.Cms.Core.Logging;
+using Umbraco.Cms.Core.Runtime;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+using Umbraco.Cms.Infrastructure.BackgroundJobs;
+using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs;
+using Umbraco.Cms.Infrastructure.Scoping;
+using Umbraco.Cms.Tests.Common;
+
+namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.BackgroundJobs.Jobs;
+
+[TestFixture]
+public class HealthCheckNotifierJobTests
+{
+ private Mock _mockNotificationMethod;
+
+ private const string Check1Id = "00000000-0000-0000-0000-000000000001";
+ private const string Check2Id = "00000000-0000-0000-0000-000000000002";
+ private const string Check3Id = "00000000-0000-0000-0000-000000000003";
+
+ [Test]
+ public async Task Does_Not_Execute_When_Not_Enabled()
+ {
+ var sut = CreateHealthCheckNotifier(false);
+ await sut.RunJobAsync();
+ VerifyNotificationsNotSent();
+ }
+
+ [Test]
+ public async Task Does_Not_Execute_With_No_Enabled_Notification_Methods()
+ {
+ var sut = CreateHealthCheckNotifier(notificationEnabled: false);
+ await sut.RunJobAsync();
+ VerifyNotificationsNotSent();
+ }
+
+ [Test]
+ public async Task Executes_With_Enabled_Notification_Methods()
+ {
+ var sut = CreateHealthCheckNotifier();
+ await sut.RunJobAsync();
+ VerifyNotificationsSent();
+ }
+
+ [Test]
+ public async Task Executes_Only_Enabled_Checks()
+ {
+ var sut = CreateHealthCheckNotifier();
+ await sut.RunJobAsync();
+ _mockNotificationMethod.Verify(
+ x => x.SendAsync(
+ It.Is(y =>
+ y.ResultsAsDictionary.Count == 1 && y.ResultsAsDictionary.ContainsKey("Check1"))),
+ Times.Once);
+ }
+
+ private HealthCheckNotifierJob CreateHealthCheckNotifier(
+ bool enabled = true,
+ bool notificationEnabled = true)
+ {
+ var settings = new HealthChecksSettings
+ {
+ Notification = new HealthChecksNotificationSettings
+ {
+ Enabled = enabled,
+ DisabledChecks = new List { new() { Id = Guid.Parse(Check3Id) } },
+ },
+ DisabledChecks = new List { new() { Id = Guid.Parse(Check2Id) } },
+ };
+ var checks = new HealthCheckCollection(() => new List
+ {
+ new TestHealthCheck1(),
+ new TestHealthCheck2(),
+ new TestHealthCheck3(),
+ });
+
+ _mockNotificationMethod = new Mock();
+ _mockNotificationMethod.SetupGet(x => x.Enabled).Returns(notificationEnabled);
+ var notifications = new HealthCheckNotificationMethodCollection(() =>
+ new List | |