diff --git a/Directory.Build.props b/Directory.Build.props
index 4c315660e6e6..b9439a50eae4 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -49,4 +49,13 @@
$(MSBuildThisFileDirectory)
+
+
+
+
+ <_ProjectReferencesWithVersions Condition="'%(ProjectVersion)' != ''">
+ [%(ProjectVersion), $([MSBuild]::Add($([System.Text.RegularExpressions.Regex]::Match('%(ProjectVersion)', '^\d+').Value), 1)))
+
+
+
diff --git a/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs b/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs
index 73e5bb4a53b8..5b191b526215 100644
--- a/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs
@@ -1,16 +1,14 @@
using Examine;
-using Examine.Lucene.Providers;
-using Examine.Lucene.Search;
using Examine.Search;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Umbraco.Cms.Api.Delivery.Services.QueryBuilders;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Infrastructure.Examine;
-using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Delivery.Services;
@@ -21,10 +19,10 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider
{
private const string ItemIdFieldName = "itemId";
private readonly IExamineManager _examineManager;
- private readonly DeliveryApiSettings _deliveryApiSettings;
private readonly ILogger _logger;
- private readonly string _fallbackGuidValue;
- private readonly Dictionary _fieldTypes;
+ private readonly ApiContentQuerySelectorBuilder _selectorBuilder;
+ private readonly ApiContentQueryFilterBuilder _filterBuilder;
+ private readonly ApiContentQuerySortBuilder _sortBuilder;
public ApiContentQueryProvider(
IExamineManager examineManager,
@@ -33,18 +31,20 @@ public ApiContentQueryProvider(
ILogger logger)
{
_examineManager = examineManager;
- _deliveryApiSettings = deliveryApiSettings.Value;
_logger = logger;
- // A fallback value is needed for Examine queries in case we don't have a value - we can't pass null or empty string
- // It is set to a random guid since this would be highly unlikely to yield any results
- _fallbackGuidValue = Guid.NewGuid().ToString("D");
-
// build a look-up dictionary of field types by field name
- _fieldTypes = indexHandlers
+ var fieldTypes = indexHandlers
.SelectMany(handler => handler.GetFields())
.DistinctBy(field => field.FieldName)
.ToDictionary(field => field.FieldName, field => field.FieldType, StringComparer.InvariantCultureIgnoreCase);
+
+ // for the time being we're going to keep these as internal implementation details.
+ // perhaps later on it will make sense to expose them through the DI.
+ _selectorBuilder = new ApiContentQuerySelectorBuilder(deliveryApiSettings.Value);
+ _filterBuilder = new ApiContentQueryFilterBuilder(fieldTypes, _logger);
+ _sortBuilder = new ApiContentQuerySortBuilder(fieldTypes, _logger);
+
}
[Obsolete($"Use the {nameof(ExecuteQuery)} method that accepts {nameof(ProtectedAccess)}. Will be removed in V14.")]
@@ -75,10 +75,9 @@ public PagedModel ExecuteQuery(
return new PagedModel();
}
- IBooleanOperation queryOperation = BuildSelectorOperation(selectorOption, index, culture, protectedAccess, preview);
-
- ApplyFiltering(filterOptions, queryOperation);
- ApplySorting(sortOptions, queryOperation);
+ IBooleanOperation queryOperation = _selectorBuilder.Build(selectorOption, index, culture, protectedAccess, preview);
+ _filterBuilder.Append(filterOptions, queryOperation);
+ _sortBuilder.Append(sortOptions, queryOperation);
ISearchResults? results = queryOperation
.SelectField(ItemIdFieldName)
@@ -102,162 +101,4 @@ public PagedModel ExecuteQuery(
{
FieldName = UmbracoExamineFieldNames.CategoryFieldName, Values = new[] { "content" }
};
-
- private IBooleanOperation BuildSelectorOperation(SelectorOption selectorOption, IIndex index, string culture, ProtectedAccess protectedAccess, bool preview)
- {
- // Needed for enabling leading wildcards searches
- BaseLuceneSearcher searcher = index.Searcher as BaseLuceneSearcher ?? throw new InvalidOperationException($"Index searcher must be of type {nameof(BaseLuceneSearcher)}.");
-
- IQuery query = searcher.CreateQuery(
- IndexTypes.Content,
- BooleanOperation.And,
- searcher.LuceneAnalyzer,
- new LuceneSearchOptions { AllowLeadingWildcard = true });
-
- IBooleanOperation selectorOperation = selectorOption.Values.Length == 1
- ? query.Field(selectorOption.FieldName, selectorOption.Values.First())
- : query.GroupedOr(new[] { selectorOption.FieldName }, selectorOption.Values);
-
- AddCultureQuery(culture, selectorOperation);
-
- if (_deliveryApiSettings.MemberAuthorizationIsEnabled())
- {
- AddProtectedAccessQuery(protectedAccess, selectorOperation);
- }
-
- // when not fetching for preview, make sure the "published" field is "y"
- if (preview is false)
- {
- selectorOperation.And().Field(UmbracoExamineFieldNames.DeliveryApiContentIndex.Published, "y");
- }
-
- return selectorOperation;
- }
-
- private void AddCultureQuery(string culture, IBooleanOperation selectorOperation) =>
- selectorOperation
- .And()
- .GroupedOr(
- // Item culture must be either the requested culture or "none"
- new[] { UmbracoExamineFieldNames.DeliveryApiContentIndex.Culture },
- culture.ToLowerInvariant().IfNullOrWhiteSpace(_fallbackGuidValue),
- "none");
-
- private void AddProtectedAccessQuery(ProtectedAccess protectedAccess, IBooleanOperation selectorOperation)
- {
- var protectedAccessValues = new List();
- if (protectedAccess.MemberKey is not null)
- {
- protectedAccessValues.Add($"u:{protectedAccess.MemberKey}");
- }
-
- if (protectedAccess.MemberRoles?.Any() is true)
- {
- protectedAccessValues.AddRange(protectedAccess.MemberRoles.Select(r => $"r:{r}"));
- }
-
- if (protectedAccessValues.Any())
- {
- selectorOperation.And(
- inner => inner
- .Field(UmbracoExamineFieldNames.DeliveryApiContentIndex.Protected, "n")
- .Or(protectedAccessInner => protectedAccessInner
- .GroupedOr(
- new[] { UmbracoExamineFieldNames.DeliveryApiContentIndex.ProtectedAccess },
- protectedAccessValues.ToArray())),
- BooleanOperation.Or);
- }
- else
- {
- selectorOperation.And().Field(UmbracoExamineFieldNames.DeliveryApiContentIndex.Protected, "n");
- }
- }
-
- private void ApplyFiltering(IList filterOptions, IBooleanOperation queryOperation)
- {
- void HandleExact(IQuery query, string fieldName, string[] values)
- {
- if (values.Length == 1)
- {
- query.Field(fieldName, values[0]);
- }
- else
- {
- query.GroupedOr(new[] { fieldName }, values);
- }
- }
-
- void HandleContains(IQuery query, string fieldName, string[] values)
- {
- if (values.Length == 1)
- {
- // The trailing wildcard is added automatically
- query.Field(fieldName, (IExamineValue)new ExamineValue(Examineness.ComplexWildcard, $"*{values[0]}"));
- }
- else
- {
- // The trailing wildcard is added automatically
- IExamineValue[] examineValues = values
- .Select(value => (IExamineValue)new ExamineValue(Examineness.ComplexWildcard, $"*{value}"))
- .ToArray();
- query.GroupedOr(new[] { fieldName }, examineValues);
- }
- }
-
- foreach (FilterOption filterOption in filterOptions)
- {
- var values = filterOption.Values.Any()
- ? filterOption.Values
- : new[] { _fallbackGuidValue };
-
- switch (filterOption.Operator)
- {
- case FilterOperation.Is:
- HandleExact(queryOperation.And(), filterOption.FieldName, values);
- break;
- case FilterOperation.IsNot:
- HandleExact(queryOperation.Not(), filterOption.FieldName, values);
- break;
- case FilterOperation.Contains:
- HandleContains(queryOperation.And(), filterOption.FieldName, values);
- break;
- case FilterOperation.DoesNotContain:
- HandleContains(queryOperation.Not(), filterOption.FieldName, values);
- break;
- default:
- continue;
- }
- }
- }
-
- private void ApplySorting(IList sortOptions, IOrdering ordering)
- {
- foreach (SortOption sort in sortOptions)
- {
- if (_fieldTypes.TryGetValue(sort.FieldName, out FieldType fieldType) is false)
- {
- _logger.LogWarning(
- "Sort implementation for field name {FieldName} does not match an index handler implementation, cannot resolve field type.",
- sort.FieldName);
- continue;
- }
-
- SortType sortType = fieldType switch
- {
- FieldType.Number => SortType.Int,
- FieldType.Date => SortType.Long,
- FieldType.StringRaw => SortType.String,
- FieldType.StringAnalyzed => SortType.String,
- FieldType.StringSortable => SortType.String,
- _ => throw new ArgumentOutOfRangeException(nameof(fieldType))
- };
-
- ordering = sort.Direction switch
- {
- Direction.Ascending => ordering.OrderBy(new SortableField(sort.FieldName, sortType)),
- Direction.Descending => ordering.OrderByDescending(new SortableField(sort.FieldName, sortType)),
- _ => ordering
- };
- }
- }
}
diff --git a/src/Umbraco.Cms.Api.Delivery/Services/QueryBuilders/ApiContentQueryFilterBuilder.cs b/src/Umbraco.Cms.Api.Delivery/Services/QueryBuilders/ApiContentQueryFilterBuilder.cs
new file mode 100644
index 000000000000..a54965f11608
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Delivery/Services/QueryBuilders/ApiContentQueryFilterBuilder.cs
@@ -0,0 +1,177 @@
+using Examine.Search;
+using Microsoft.Extensions.Logging;
+using Umbraco.Cms.Core.DeliveryApi;
+
+namespace Umbraco.Cms.Api.Delivery.Services.QueryBuilders;
+
+internal sealed class ApiContentQueryFilterBuilder
+{
+ private readonly IDictionary _fieldTypes;
+ private readonly ILogger _logger;
+ private readonly string _fallbackGuidValue;
+
+ public ApiContentQueryFilterBuilder(IDictionary fieldTypes, ILogger logger)
+ {
+ _fieldTypes = fieldTypes;
+ _logger = logger;
+
+ // A fallback value is needed for Examine queries in case we don't have a value - we can't pass null or empty string
+ // It is set to a random guid since this would be highly unlikely to yield any results
+ _fallbackGuidValue = Guid.NewGuid().ToString("D");
+ }
+
+ public void Append(IList filterOptions, IBooleanOperation queryOperation)
+ {
+ foreach (FilterOption filterOption in filterOptions)
+ {
+ if (_fieldTypes.TryGetValue(filterOption.FieldName, out FieldType fieldType) is false)
+ {
+ _logger.LogWarning(
+ "Filter implementation for field name {FieldName} does not match an index handler implementation, cannot resolve field type.",
+ filterOption.FieldName);
+ continue;
+ }
+
+ var values = filterOption.Values.Any()
+ ? filterOption.Values
+ : new[] { _fallbackGuidValue };
+
+ switch (filterOption.Operator)
+ {
+ case FilterOperation.Is:
+ ApplyExactFilter(queryOperation.And(), filterOption.FieldName, values, fieldType);
+ break;
+ case FilterOperation.IsNot:
+ ApplyExactFilter(queryOperation.Not(), filterOption.FieldName, values, fieldType);
+ break;
+ case FilterOperation.Contains:
+ ApplyContainsFilter(queryOperation.And(), filterOption.FieldName, values);
+ break;
+ case FilterOperation.DoesNotContain:
+ ApplyContainsFilter(queryOperation.Not(), filterOption.FieldName, values);
+ break;
+ default:
+ continue;
+ }
+ }
+ }
+
+ private void ApplyExactFilter(IQuery query, string fieldName, string[] values, FieldType fieldType)
+ {
+ switch (fieldType)
+ {
+ case FieldType.Number:
+ ApplyExactNumberFilter(query, fieldName, values);
+ break;
+ case FieldType.Date:
+ ApplyExactDateFilter(query, fieldName, values);
+ break;
+ default:
+ ApplyExactStringFilter(query, fieldName, values);
+ break;
+ }
+ }
+
+ private void ApplyExactNumberFilter(IQuery query, string fieldName, string[] values)
+ {
+ if (values.Length == 1)
+ {
+ if (TryParseIntFilterValue(values.First(), out int intValue))
+ {
+ query.Field(fieldName, intValue);
+ }
+ }
+ else
+ {
+ int[] intValues = values
+ .Select(value => TryParseIntFilterValue(value, out int intValue) ? intValue : (int?)null)
+ .Where(intValue => intValue.HasValue)
+ .Select(intValue => intValue!.Value)
+ .ToArray();
+
+ AddGroupedOrFilter(query, fieldName, intValues);
+ }
+ }
+
+ private void ApplyExactDateFilter(IQuery query, string fieldName, string[] values)
+ {
+ if (values.Length == 1)
+ {
+ if (TryParseDateTimeFilterValue(values.First(), out DateTime dateValue))
+ {
+ query.Field(fieldName, dateValue);
+ }
+ }
+ else
+ {
+ DateTime[] dateValues = values
+ .Select(value => TryParseDateTimeFilterValue(value, out DateTime dateValue) ? dateValue : (DateTime?)null)
+ .Where(dateValue => dateValue.HasValue)
+ .Select(dateValue => dateValue!.Value)
+ .ToArray();
+
+ AddGroupedOrFilter(query, fieldName, dateValues);
+ }
+ }
+
+ private void ApplyExactStringFilter(IQuery query, string fieldName, string[] values)
+ {
+ if (values.Length == 1)
+ {
+ query.Field(fieldName, values.First());
+ }
+ else
+ {
+ AddGroupedOrFilter(query, fieldName, values);
+ }
+ }
+
+ private void ApplyContainsFilter(IQuery query, string fieldName, string[] values)
+ {
+ if (values.Length == 1)
+ {
+ // The trailing wildcard is added automatically
+ query.Field(fieldName, (IExamineValue)new ExamineValue(Examineness.ComplexWildcard, $"*{values[0]}"));
+ }
+ else
+ {
+ // The trailing wildcard is added automatically
+ IExamineValue[] examineValues = values
+ .Select(value => (IExamineValue)new ExamineValue(Examineness.ComplexWildcard, $"*{value}"))
+ .ToArray();
+ query.GroupedOr(new[] { fieldName }, examineValues);
+ }
+ }
+
+ private void AddGroupedOrFilter(IQuery query, string fieldName, params T[] values)
+ where T : struct
+ {
+ if (values.Length == 0)
+ {
+ return;
+ }
+
+ var fields = new[] { fieldName };
+ query.Group(
+ nestedQuery =>
+ {
+ INestedBooleanOperation nestedQueryOperation =
+ nestedQuery.RangeQuery(fields, values[0], values[0]);
+ foreach (T value in values.Skip(1))
+ {
+ nestedQueryOperation = nestedQueryOperation.Or().RangeQuery(fields, value, value);
+ }
+
+ return nestedQueryOperation;
+ });
+ }
+
+ private void AddGroupedOrFilter(IQuery query, string fieldName, params string[] values)
+ => query.GroupedOr(new[] { fieldName }, values);
+
+ private bool TryParseIntFilterValue(string value, out int intValue)
+ => int.TryParse(value, out intValue);
+
+ private bool TryParseDateTimeFilterValue(string value, out DateTime dateValue)
+ => DateTime.TryParse(value, out dateValue);
+}
diff --git a/src/Umbraco.Cms.Api.Delivery/Services/QueryBuilders/ApiContentQuerySelectorBuilder.cs b/src/Umbraco.Cms.Api.Delivery/Services/QueryBuilders/ApiContentQuerySelectorBuilder.cs
new file mode 100644
index 000000000000..c39f6f3f51b2
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Delivery/Services/QueryBuilders/ApiContentQuerySelectorBuilder.cs
@@ -0,0 +1,96 @@
+using Examine;
+using Examine.Lucene.Providers;
+using Examine.Lucene.Search;
+using Examine.Search;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.DeliveryApi;
+using Umbraco.Cms.Core.Models.DeliveryApi;
+using Umbraco.Cms.Infrastructure.Examine;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Api.Delivery.Services.QueryBuilders;
+
+internal sealed class ApiContentQuerySelectorBuilder
+{
+ private readonly DeliveryApiSettings _deliveryApiSettings;
+ private readonly string _fallbackGuidValue;
+
+ public ApiContentQuerySelectorBuilder(DeliveryApiSettings deliveryApiSettings)
+ {
+ _deliveryApiSettings = deliveryApiSettings;
+
+ // A fallback value is needed for Examine queries in case we don't have a value - we can't pass null or empty string
+ // It is set to a random guid since this would be highly unlikely to yield any results
+ _fallbackGuidValue = Guid.NewGuid().ToString("D");
+ }
+
+ public IBooleanOperation Build(SelectorOption selectorOption, IIndex index, string culture, ProtectedAccess protectedAccess, bool preview)
+ {
+ // Needed for enabling leading wildcards searches
+ BaseLuceneSearcher searcher = index.Searcher as BaseLuceneSearcher ?? throw new InvalidOperationException($"Index searcher must be of type {nameof(BaseLuceneSearcher)}.");
+
+ IQuery query = searcher.CreateQuery(
+ IndexTypes.Content,
+ BooleanOperation.And,
+ searcher.LuceneAnalyzer,
+ new LuceneSearchOptions { AllowLeadingWildcard = true });
+
+ IBooleanOperation selectorOperation = selectorOption.Values.Length == 1
+ ? query.Field(selectorOption.FieldName, selectorOption.Values.First())
+ : query.GroupedOr(new[] { selectorOption.FieldName }, selectorOption.Values);
+
+ AddCultureQuery(culture, selectorOperation);
+
+ if (_deliveryApiSettings.MemberAuthorizationIsEnabled())
+ {
+ AddProtectedAccessQuery(protectedAccess, selectorOperation);
+ }
+
+ // when not fetching for preview, make sure the "published" field is "y"
+ if (preview is false)
+ {
+ selectorOperation.And().Field(UmbracoExamineFieldNames.DeliveryApiContentIndex.Published, "y");
+ }
+
+ return selectorOperation;
+ }
+
+ private void AddCultureQuery(string culture, IBooleanOperation selectorOperation) =>
+ selectorOperation
+ .And()
+ .GroupedOr(
+ // Item culture must be either the requested culture or "none"
+ new[] { UmbracoExamineFieldNames.DeliveryApiContentIndex.Culture },
+ culture.ToLowerInvariant().IfNullOrWhiteSpace(_fallbackGuidValue),
+ "none");
+
+ private void AddProtectedAccessQuery(ProtectedAccess protectedAccess, IBooleanOperation selectorOperation)
+ {
+ var protectedAccessValues = new List();
+ if (protectedAccess.MemberKey is not null)
+ {
+ protectedAccessValues.Add($"u:{protectedAccess.MemberKey}");
+ }
+
+ if (protectedAccess.MemberRoles?.Any() is true)
+ {
+ protectedAccessValues.AddRange(protectedAccess.MemberRoles.Select(r => $"r:{r}"));
+ }
+
+ if (protectedAccessValues.Any())
+ {
+ selectorOperation.And(
+ inner => inner
+ .Field(UmbracoExamineFieldNames.DeliveryApiContentIndex.Protected, "n")
+ .Or(protectedAccessInner => protectedAccessInner
+ .GroupedOr(
+ new[] { UmbracoExamineFieldNames.DeliveryApiContentIndex.ProtectedAccess },
+ protectedAccessValues.ToArray())),
+ BooleanOperation.Or);
+ }
+ else
+ {
+ selectorOperation.And().Field(UmbracoExamineFieldNames.DeliveryApiContentIndex.Protected, "n");
+ }
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Delivery/Services/QueryBuilders/ApiContentQuerySortBuilder.cs b/src/Umbraco.Cms.Api.Delivery/Services/QueryBuilders/ApiContentQuerySortBuilder.cs
new file mode 100644
index 000000000000..92ed501597a3
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Delivery/Services/QueryBuilders/ApiContentQuerySortBuilder.cs
@@ -0,0 +1,49 @@
+using Examine.Search;
+using Microsoft.Extensions.Logging;
+using Umbraco.Cms.Core;
+using Umbraco.Cms.Core.DeliveryApi;
+
+namespace Umbraco.Cms.Api.Delivery.Services.QueryBuilders;
+
+internal sealed class ApiContentQuerySortBuilder
+{
+ private readonly Dictionary _fieldTypes;
+ private readonly ILogger _logger;
+
+ public ApiContentQuerySortBuilder(Dictionary fieldTypes, ILogger logger)
+ {
+ _fieldTypes = fieldTypes;
+ _logger = logger;
+ }
+
+ public void Append(IList sortOptions, IOrdering queryOperation)
+ {
+ foreach (SortOption sort in sortOptions)
+ {
+ if (_fieldTypes.TryGetValue(sort.FieldName, out FieldType fieldType) is false)
+ {
+ _logger.LogWarning(
+ "Sort implementation for field name {FieldName} does not match an index handler implementation, cannot resolve field type.",
+ sort.FieldName);
+ continue;
+ }
+
+ SortType sortType = fieldType switch
+ {
+ FieldType.Number => SortType.Int,
+ FieldType.Date => SortType.Long,
+ FieldType.StringRaw => SortType.String,
+ FieldType.StringAnalyzed => SortType.String,
+ FieldType.StringSortable => SortType.String,
+ _ => throw new ArgumentOutOfRangeException(nameof(fieldType))
+ };
+
+ queryOperation = sort.Direction switch
+ {
+ Direction.Ascending => queryOperation.OrderBy(new SortableField(sort.FieldName, sortType)),
+ Direction.Descending => queryOperation.OrderByDescending(new SortableField(sort.FieldName, sortType)),
+ _ => queryOperation
+ };
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Constants-WebhookEvents.cs b/src/Umbraco.Core/Constants-WebhookEvents.cs
index afd57b5188ef..41b9849f08f1 100644
--- a/src/Umbraco.Core/Constants-WebhookEvents.cs
+++ b/src/Umbraco.Core/Constants-WebhookEvents.cs
@@ -6,6 +6,57 @@ public static class WebhookEvents
{
public static class Aliases
{
+
+ ///
+ /// Webhook event alias for content versions deleted
+ ///
+ public const string ContentDeletedVersions = "Umbraco.ContentDeletedVersions";
+
+ ///
+ /// Webhook event alias for content blueprint saved
+ ///
+ public const string ContentSavedBlueprint = "Umbraco.ContentSavedBlueprint";
+
+ ///
+ /// Webhook event alias for content blueprint deleted
+ ///
+ public const string ContentDeletedBlueprint = "Umbraco.ContentDeletedBlueprint";
+
+ ///
+ /// Webhook event alias for content moved into the recycle bin.
+ ///
+ public const string ContentMovedToRecycleBin = "Umbraco.ContentMovedToRecycleBin";
+
+ ///
+ /// Webhook event alias for content sorted.
+ ///
+ public const string ContentSorted = "Umbraco.ContentSorted";
+
+ ///
+ /// Webhook event alias for content moved.
+ ///
+ public const string ContentMoved = "Umbraco.ContentMoved";
+
+ ///
+ /// Webhook event alias for content copied.
+ ///
+ public const string ContentCopied = "Umbraco.ContentCopied";
+
+ ///
+ /// Webhook event alias for content emptied recycle bin.
+ ///
+ public const string ContentEmptiedRecycleBin = "Umbraco.ContentEmptiedRecycleBin";
+
+ ///
+ /// Webhook event alias for content rolled back.
+ ///
+ public const string ContentRolledBack = "Umbraco.ContentRolledBack";
+
+ ///
+ /// Webhook event alias for content saved.
+ ///
+ public const string ContentSaved = "Umbraco.ContentSaved";
+
///
/// Webhook event alias for content publish.
///
diff --git a/src/Umbraco.Core/DeliveryApi/DeliveryApiCompositeIdHandler.cs b/src/Umbraco.Core/DeliveryApi/DeliveryApiCompositeIdHandler.cs
new file mode 100644
index 000000000000..a3c1f0fceb77
--- /dev/null
+++ b/src/Umbraco.Core/DeliveryApi/DeliveryApiCompositeIdHandler.cs
@@ -0,0 +1,21 @@
+namespace Umbraco.Cms.Core.DeliveryApi;
+
+public class DeliveryApiCompositeIdHandler : IDeliveryApiCompositeIdHandler
+{
+ public string IndexId(int id, string culture) => $"{id}|{culture}";
+
+ public DeliveryApiIndexCompositeIdModel Decompose(string indexId)
+ {
+ var parts = indexId.Split(Constants.CharArrays.VerticalTab);
+ if (parts.Length == 2 && int.TryParse(parts[0], out var id))
+ {
+ return new DeliveryApiIndexCompositeIdModel
+ {
+ Id = id,
+ Culture = parts[1],
+ };
+ }
+
+ return new DeliveryApiIndexCompositeIdModel();
+ }
+}
diff --git a/src/Umbraco.Core/DeliveryApi/DeliveryApiIndexCompositeIdModel.cs b/src/Umbraco.Core/DeliveryApi/DeliveryApiIndexCompositeIdModel.cs
new file mode 100644
index 000000000000..d3b876b5aac8
--- /dev/null
+++ b/src/Umbraco.Core/DeliveryApi/DeliveryApiIndexCompositeIdModel.cs
@@ -0,0 +1,8 @@
+namespace Umbraco.Cms.Core.DeliveryApi;
+
+public class DeliveryApiIndexCompositeIdModel
+{
+ public int? Id { get; set; }
+
+ public string? Culture { get; set; }
+}
diff --git a/src/Umbraco.Core/DeliveryApi/IDeliveryApiCompositeIdHandler.cs b/src/Umbraco.Core/DeliveryApi/IDeliveryApiCompositeIdHandler.cs
new file mode 100644
index 000000000000..a472d0f9ffc2
--- /dev/null
+++ b/src/Umbraco.Core/DeliveryApi/IDeliveryApiCompositeIdHandler.cs
@@ -0,0 +1,8 @@
+namespace Umbraco.Cms.Core.DeliveryApi;
+
+public interface IDeliveryApiCompositeIdHandler
+{
+ string IndexId(int id, string culture);
+
+ DeliveryApiIndexCompositeIdModel Decompose(string indexId);
+}
diff --git a/src/Umbraco.Core/Deploy/ArtifactBase.cs b/src/Umbraco.Core/Deploy/ArtifactBase.cs
index 0d354b65de80..994297990d6b 100644
--- a/src/Umbraco.Core/Deploy/ArtifactBase.cs
+++ b/src/Umbraco.Core/Deploy/ArtifactBase.cs
@@ -1,49 +1,69 @@
namespace Umbraco.Cms.Core.Deploy;
///
-/// Provides a base class to all artifacts.
+/// Provides a base class for all artifacts.
///
+/// The UDI type.
public abstract class ArtifactBase : IArtifact
where TUdi : Udi
{
+ private IEnumerable _dependencies;
+ private readonly Lazy _checksum;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The UDI.
+ /// The dependencies.
protected ArtifactBase(TUdi udi, IEnumerable? dependencies = null)
{
- Udi = udi ?? throw new ArgumentNullException("udi");
+ Udi = udi ?? throw new ArgumentNullException(nameof(udi));
Name = Udi.ToString();
_dependencies = dependencies ?? Enumerable.Empty();
_checksum = new Lazy(GetChecksum);
}
- private readonly Lazy _checksum;
-
- private IEnumerable _dependencies;
-
- protected abstract string GetChecksum();
-
+ ///
Udi IArtifactSignature.Udi => Udi;
+ ///
public TUdi Udi { get; set; }
+ ///
+ public IEnumerable Dependencies
+ {
+ get => _dependencies;
+ set => _dependencies = value.OrderBy(x => x.Udi);
+ }
+
+ ///
public string Checksum => _checksum.Value;
+ ///
+ public string Name { get; set; }
+
+ ///
+ public string Alias { get; set; } = string.Empty;
+
+ ///
+ /// Gets the checksum.
+ ///
+ ///
+ /// The checksum.
+ ///
+ protected abstract string GetChecksum();
+
///
/// Prevents the property from being serialized.
///
+ ///
+ /// Returns false to prevent the property from being serialized.
+ ///
///
- /// Note that we can't use here as that works only on fields, not properties. And we want to avoid using [JsonIgnore]
+ /// Note that we can't use here as that works only on fields, not properties. And we want to avoid using [JsonIgnore]
/// as that would require an external dependency in Umbraco.Cms.Core.
/// So using this method of excluding properties from serialized data, documented here: https://www.newtonsoft.com/json/help/html/ConditionalProperties.htm
///
public bool ShouldSerializeChecksum() => false;
-
- public IEnumerable Dependencies
- {
- get => _dependencies;
- set => _dependencies = value.OrderBy(x => x.Udi);
- }
-
- public string Name { get; set; }
-
- public string Alias { get; set; } = string.Empty;
}
diff --git a/src/Umbraco.Core/Deploy/ArtifactDependency.cs b/src/Umbraco.Core/Deploy/ArtifactDependency.cs
index 31c8025ddbac..80a77740d0c9 100644
--- a/src/Umbraco.Core/Deploy/ArtifactDependency.cs
+++ b/src/Umbraco.Core/Deploy/ArtifactDependency.cs
@@ -1,27 +1,17 @@
-using System.Text.Json.Serialization;
-
namespace Umbraco.Cms.Core.Deploy;
///
/// Represents an artifact dependency.
///
-///
-///
-/// Dependencies have an order property which indicates whether it must be respected when ordering artifacts.
-///
-///
-/// Dependencies have a mode which can be or depending on whether the checksum should match.
-///
-///
public class ArtifactDependency
{
///
/// Initializes a new instance of the class.
///
/// The entity identifier of the artifact dependency.
- /// A value indicating whether the dependency is ordering.
- /// The dependency mode.
- /// The checksum.
+ /// A value indicating whether the dependency must be included when building a dependency tree and ensure the artifact gets deployed in the correct order.
+ /// A value indicating whether the checksum must match or the artifact just needs to exist.
+ /// The checksum of the dependency.
public ArtifactDependency(Udi udi, bool ordering, ArtifactDependencyMode mode, string? checksum = null)
{
Udi = udi;
@@ -39,10 +29,10 @@ public ArtifactDependency(Udi udi, bool ordering, ArtifactDependencyMode mode, s
public Udi Udi { get; }
///
- /// Gets a value indicating whether the dependency is ordering.
+ /// Gets a value indicating whether the dependency is included when building a dependency tree and gets deployed in the correct order.
///
///
- /// true if the dependency is ordering; otherwise, false.
+ /// true if the dependency is included when building a dependency tree and gets deployed in the correct order; otherwise, false.
///
public bool Ordering { get; }
@@ -55,10 +45,10 @@ public ArtifactDependency(Udi udi, bool ordering, ArtifactDependencyMode mode, s
public ArtifactDependencyMode Mode { get; }
///
- /// Gets the checksum.
+ /// Gets or sets the checksum.
///
///
/// The checksum.
///
- public string? Checksum { get; }
+ public string? Checksum { get; set; }
}
diff --git a/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs
index 1be524c86f5e..7f2b05eaad01 100644
--- a/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs
+++ b/src/Umbraco.Core/Deploy/ArtifactDependencyCollection.cs
@@ -3,42 +3,53 @@
namespace Umbraco.Cms.Core.Deploy;
///
-/// Represents a collection of distinct .
+/// Represents a collection of distinct .
///
-/// The collection cannot contain duplicates and modes are properly managed.
+///
+/// The collection cannot contain duplicates and modes are properly managed.
+///
public class ArtifactDependencyCollection : ICollection
{
private readonly Dictionary _dependencies = new();
+ ///
public int Count => _dependencies.Count;
- public IEnumerator GetEnumerator() => _dependencies.Values.GetEnumerator();
-
- IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+ ///
+ public bool IsReadOnly => false;
+ ///
public void Add(ArtifactDependency item)
{
- if (_dependencies.ContainsKey(item.Udi))
+ if (item.Mode == ArtifactDependencyMode.Exist &&
+ _dependencies.TryGetValue(item.Udi, out ArtifactDependency? existingItem) &&
+ existingItem.Mode == ArtifactDependencyMode.Match)
{
- ArtifactDependency exist = _dependencies[item.Udi];
- if (item.Mode == ArtifactDependencyMode.Exist || item.Mode == exist.Mode)
- {
- return;
- }
+ // Don't downgrade dependency mode from Match to Exist
+ return;
}
_dependencies[item.Udi] = item;
}
+ ///
public void Clear() => _dependencies.Clear();
- public bool Contains(ArtifactDependency item) =>
- _dependencies.ContainsKey(item.Udi) &&
- (_dependencies[item.Udi].Mode == item.Mode || _dependencies[item.Udi].Mode == ArtifactDependencyMode.Match);
+ ///
+ public bool Contains(ArtifactDependency item)
+ => _dependencies.TryGetValue(item.Udi, out ArtifactDependency? existingItem) &&
+ // Check whether it has the same or higher dependency mode
+ (existingItem.Mode == item.Mode || existingItem.Mode == ArtifactDependencyMode.Match);
+ ///
public void CopyTo(ArtifactDependency[] array, int arrayIndex) => _dependencies.Values.CopyTo(array, arrayIndex);
- public bool Remove(ArtifactDependency item) => throw new NotSupportedException();
+ ///
+ public bool Remove(ArtifactDependency item) => _dependencies.Remove(item.Udi);
- public bool IsReadOnly => false;
+ ///
+ public IEnumerator GetEnumerator() => _dependencies.Values.GetEnumerator();
+
+ ///
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
diff --git a/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs b/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs
index b997b9c75970..6ea6a5e2c29e 100644
--- a/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs
+++ b/src/Umbraco.Core/Deploy/ArtifactDependencyMode.cs
@@ -1,17 +1,16 @@
namespace Umbraco.Cms.Core.Deploy;
///
-/// Indicates the mode of the dependency.
+/// Indicates the mode of the dependency.
///
public enum ArtifactDependencyMode
{
///
- /// The dependency must match exactly.
+ /// The dependency must match exactly.
///
Match,
-
///
- /// The dependency must exist.
+ /// The dependency must exist.
///
Exist,
}
diff --git a/src/Umbraco.Core/Deploy/ArtifactDeployState.cs b/src/Umbraco.Core/Deploy/ArtifactDeployState.cs
index e2dd343af1aa..b6a31f2da236 100644
--- a/src/Umbraco.Core/Deploy/ArtifactDeployState.cs
+++ b/src/Umbraco.Core/Deploy/ArtifactDeployState.cs
@@ -1,27 +1,36 @@
namespace Umbraco.Cms.Core.Deploy;
///
-/// Represent the state of an artifact being deployed.
+/// Represent the state of an artifact being deployed.
///
public abstract class ArtifactDeployState
{
///
- /// Gets the artifact.
+ /// Gets the artifact.
///
+ ///
+ /// The artifact.
+ ///
public IArtifact Artifact => GetArtifactAsIArtifact();
///
- /// Gets or sets the service connector in charge of deploying the artifact.
+ /// Gets or sets the service connector in charge of deploying the artifact.
///
+ ///
+ /// The connector.
+ ///
public IServiceConnector? Connector { get; set; }
///
- /// Gets or sets the next pass number.
+ /// Gets or sets the next pass number.
///
+ ///
+ /// The next pass.
+ ///
public int NextPass { get; set; }
///
- /// Creates a new instance of the class from an artifact and an entity.
+ /// Creates a new instance of the class from an artifact and an entity.
///
/// The type of the artifact.
/// The type of the entity.
@@ -29,24 +38,28 @@ public abstract class ArtifactDeployState
/// The entity.
/// The service connector deploying the artifact.
/// The next pass number.
- /// A deploying artifact.
+ ///
+ /// A deploying artifact.
+ ///
public static ArtifactDeployState Create(TArtifact art, TEntity? entity, IServiceConnector connector, int nextPass)
where TArtifact : IArtifact =>
new ArtifactDeployState(art, entity, connector, nextPass);
///
- /// Gets the artifact as an .
+ /// Gets the artifact as an .
///
- /// The artifact, as an .
+ ///
+ /// The artifact, as an .
+ ///
///
- /// This is because classes that inherit from this class cannot override the Artifact property
- /// with a property that specializes the return type, and so they need to 'new' the property.
+ /// This is because classes that inherit from this class cannot override the Artifact property
+ /// with a property that specializes the return type, and so they need to 'new' the property.
///
protected abstract IArtifact GetArtifactAsIArtifact();
}
///
-/// Represent the state of an artifact being deployed.
+/// Represent the state of an artifact being deployed.
///
/// The type of the artifact.
/// The type of the entity.
@@ -54,7 +67,7 @@ public class ArtifactDeployState : ArtifactDeployState
where TArtifact : IArtifact
{
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
/// The artifact.
/// The entity.
@@ -69,13 +82,19 @@ public ArtifactDeployState(TArtifact art, TEntity? entity, IServiceConnector con
}
///
- /// Gets or sets the artifact.
+ /// Gets or sets the artifact.
///
+ ///
+ /// The artifact.
+ ///
public new TArtifact Artifact { get; set; }
///
- /// Gets or sets the entity.
+ /// Gets or sets the entity.
///
+ ///
+ /// The entity.
+ ///
public TEntity? Entity { get; set; }
///
diff --git a/src/Umbraco.Core/Deploy/ArtifactSignature.cs b/src/Umbraco.Core/Deploy/ArtifactSignature.cs
index 3dccddba2935..c91897355ec4 100644
--- a/src/Umbraco.Core/Deploy/ArtifactSignature.cs
+++ b/src/Umbraco.Core/Deploy/ArtifactSignature.cs
@@ -1,17 +1,27 @@
namespace Umbraco.Cms.Core.Deploy;
+///
public sealed class ArtifactSignature : IArtifactSignature
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The UDI.
+ /// The checksum.
+ /// The artifact dependencies.
public ArtifactSignature(Udi udi, string checksum, IEnumerable? dependencies = null)
{
Udi = udi;
Checksum = checksum;
- Dependencies = dependencies ?? Enumerable.Empty();
+ Dependencies = dependencies ?? Array.Empty();
}
+ ///
public Udi Udi { get; }
+ ///
public string Checksum { get; }
+ ///
public IEnumerable Dependencies { get; }
}
diff --git a/src/Umbraco.Core/Deploy/Difference.cs b/src/Umbraco.Core/Deploy/Difference.cs
index d704642a9f9d..7bc3ff3055b3 100644
--- a/src/Umbraco.Core/Deploy/Difference.cs
+++ b/src/Umbraco.Core/Deploy/Difference.cs
@@ -1,7 +1,16 @@
namespace Umbraco.Cms.Core.Deploy;
+///
+/// Represents a difference between two artifacts.
+///
public class Difference
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The title.
+ /// The text.
+ /// The category.
public Difference(string title, string? text = null, string? category = null)
{
Title = title;
@@ -9,12 +18,36 @@ public Difference(string title, string? text = null, string? category = null)
Category = category;
}
+ ///
+ /// Gets or sets the title.
+ ///
+ ///
+ /// The title.
+ ///
public string Title { get; set; }
+ ///
+ /// Gets or sets the text.
+ ///
+ ///
+ /// The text.
+ ///
public string? Text { get; set; }
+ ///
+ /// Gets or sets the category.
+ ///
+ ///
+ /// The category.
+ ///
public string? Category { get; set; }
+ ///
+ /// Converts the difference to a .
+ ///
+ ///
+ /// A that represents the difference.
+ ///
public override string ToString()
{
var s = Title;
diff --git a/src/Umbraco.Core/Deploy/Direction.cs b/src/Umbraco.Core/Deploy/Direction.cs
index 30439380f222..a1129e2e4ce7 100644
--- a/src/Umbraco.Core/Deploy/Direction.cs
+++ b/src/Umbraco.Core/Deploy/Direction.cs
@@ -1,7 +1,16 @@
namespace Umbraco.Cms.Core.Deploy;
+///
+/// Represents the direction when replacing an attribute value while parsing macros.
+///
public enum Direction
{
+ ///
+ /// Replacing an attribute value while converting to an artifact.
+ ///
ToArtifact,
+ ///
+ /// Replacing an attribute value while converting from an artifact.
+ ///
FromArtifact,
}
diff --git a/src/Umbraco.Core/Deploy/IArtifact.cs b/src/Umbraco.Core/Deploy/IArtifact.cs
index faea983dee8a..6b041b7f82f6 100644
--- a/src/Umbraco.Core/Deploy/IArtifact.cs
+++ b/src/Umbraco.Core/Deploy/IArtifact.cs
@@ -1,11 +1,23 @@
namespace Umbraco.Cms.Core.Deploy;
///
-/// Represents an artifact ie an object that can be transfered between environments.
+/// Represents an artifact ie an object that can be transfered between environments.
///
public interface IArtifact : IArtifactSignature
{
+ ///
+ /// Gets the name.
+ ///
+ ///
+ /// The name.
+ ///
string Name { get; }
+ ///
+ /// Gets the alias.
+ ///
+ ///
+ /// The alias.
+ ///
string? Alias { get; }
}
diff --git a/src/Umbraco.Core/Deploy/IArtifactSignature.cs b/src/Umbraco.Core/Deploy/IArtifactSignature.cs
index f1dd35295fcc..ca34fdb894c3 100644
--- a/src/Umbraco.Core/Deploy/IArtifactSignature.cs
+++ b/src/Umbraco.Core/Deploy/IArtifactSignature.cs
@@ -1,46 +1,55 @@
namespace Umbraco.Cms.Core.Deploy;
///
-/// Represents the signature of an artifact.
+/// Represents the signature of an artifact.
///
public interface IArtifactSignature
{
///
- /// Gets the entity unique identifier of this artifact.
+ /// Gets the entity unique identifier of this artifact.
///
+ ///
+ /// The udi.
+ ///
///
- ///
- /// The project identifier is independent from the state of the artifact, its data
- /// values, dependencies, anything. It never changes and fully identifies the artifact.
- ///
- ///
- /// What an entity uses as a unique identifier will influence what we can transfer
- /// between environments. Eg content type "Foo" on one environment is not necessarily the
- /// same as "Foo" on another environment, if guids are used as unique identifiers. What is
- /// used should be documented for each entity, along with the consequences of the choice.
- ///
+ ///
+ /// The project identifier is independent from the state of the artifact, its data
+ /// values, dependencies, anything. It never changes and fully identifies the artifact.
+ ///
+ ///
+ /// What an entity uses as a unique identifier will influence what we can transfer
+ /// between environments. Eg content type "Foo" on one environment is not necessarily the
+ /// same as "Foo" on another environment, if guids are used as unique identifiers. What is
+ /// used should be documented for each entity, along with the consequences of the choice.
+ ///
///
Udi Udi { get; }
///
- /// Gets the checksum of this artifact.
+ /// Gets the checksum of this artifact.
///
+ ///
+ /// The checksum.
+ ///
///
- ///
- /// The checksum depends on the artifact's properties, and on the identifiers of all its dependencies,
- /// but not on their checksums. So the checksum changes when any of the artifact's properties changes,
- /// or when the list of dependencies changes. But not if one of these dependencies change.
- ///
- ///
- /// It is assumed that checksum collisions cannot happen ie that no two different artifact's
- /// states will ever produce the same checksum, so that if two artifacts have the same checksum then
- /// they are identical.
- ///
+ ///
+ /// The checksum depends on the artifact's properties, and on the identifiers of all its dependencies,
+ /// but not on their checksums. So the checksum changes when any of the artifact's properties changes,
+ /// or when the list of dependencies changes. But not if one of these dependencies change.
+ ///
+ ///
+ /// It is assumed that checksum collisions cannot happen ie that no two different artifact's
+ /// states will ever produce the same checksum, so that if two artifacts have the same checksum then
+ /// they are identical.
+ ///
///
string Checksum { get; }
///
- /// Gets the dependencies of this artifact.
+ /// Gets the dependencies of this artifact.
///
+ ///
+ /// The dependencies.
+ ///
IEnumerable Dependencies { get; }
}
diff --git a/src/Umbraco.Core/Deploy/IDeployContext.cs b/src/Umbraco.Core/Deploy/IDeployContext.cs
index bdc8fd8d61d4..f706cbd3ae19 100644
--- a/src/Umbraco.Core/Deploy/IDeployContext.cs
+++ b/src/Umbraco.Core/Deploy/IDeployContext.cs
@@ -1,39 +1,56 @@
namespace Umbraco.Cms.Core.Deploy;
///
-/// Represents a deployment context.
+/// Represents a deployment context.
///
public interface IDeployContext
{
///
- /// Gets the unique identifier of the deployment.
+ /// Gets the unique identifier of the deployment.
///
+ ///
+ /// The session identifier.
+ ///
Guid SessionId { get; }
///
- /// Gets the file source.
+ /// Gets the file source.
///
- /// The file source is used to obtain files from the source environment.
+ ///
+ /// The file source.
+ ///
+ ///
+ /// The file source is used to obtain files from the source environment.
+ ///
IFileSource FileSource { get; }
///
- /// Gets items.
+ /// Gets items.
///
+ ///
+ /// The items.
+ ///
IDictionary Items { get; }
///
- /// Gets the next number in a numerical sequence.
+ /// Gets the next number in a numerical sequence.
///
- /// The next sequence number.
- /// Can be used to uniquely number things during a deployment.
+ ///
+ /// The next sequence number.
+ ///
+ ///
+ /// Can be used to uniquely number things during a deployment.
+ ///
int NextSeq();
///
- /// Gets item.
+ /// Gets item.
///
/// The type of the item.
/// The key of the item.
- /// The item with the specified key and type, if any, else null.
+ ///
+ /// The item with the specified key and type, if any, else null.
+ ///
T? Item(string key)
where T : class;
diff --git a/src/Umbraco.Core/Deploy/IFileSource.cs b/src/Umbraco.Core/Deploy/IFileSource.cs
index d3f6ebe77072..92eb125d9e23 100644
--- a/src/Umbraco.Core/Deploy/IFileSource.cs
+++ b/src/Umbraco.Core/Deploy/IFileSource.cs
@@ -1,70 +1,86 @@
namespace Umbraco.Cms.Core.Deploy;
///
-/// Represents a file source, ie a mean for a target environment involved in a
-/// deployment to obtain the content of files being deployed.
+/// Represents a file source, ie a mean for a target environment involved in a
+/// deployment to obtain the content of files being deployed.
///
public interface IFileSource
{
///
- /// Gets the content of a file as a stream.
+ /// Gets the content of a file as a stream.
///
/// A file entity identifier.
- /// A stream with read access to the file content.
+ ///
+ /// A stream with read access to the file content.
+ ///
///
- /// Returns null if no content could be read.
- /// The caller should ensure that the stream is properly closed/disposed.
+ /// Returns null if no content could be read.
+ /// The caller should ensure that the stream is properly closed/disposed.
///
Stream GetFileStream(StringUdi udi);
///
- /// Gets the content of a file as a stream.
+ /// Gets the content of a file as a stream.
///
/// A file entity identifier.
/// A cancellation token.
- /// A stream with read access to the file content.
+ ///
+ /// A stream with read access to the file content.
+ ///
///
- /// Returns null if no content could be read.
- /// The caller should ensure that the stream is properly closed/disposed.
+ /// Returns null if no content could be read.
+ /// The caller should ensure that the stream is properly closed/disposed.
///
Task GetFileStreamAsync(StringUdi udi, CancellationToken token);
///
- /// Gets the content of a file as a string.
+ /// Gets the content of a file as a string.
///
/// A file entity identifier.
- /// A string containing the file content.
- /// Returns null if no content could be read.
+ ///
+ /// A string containing the file content.
+ ///
+ ///
+ /// Returns null if no content could be read.
+ ///
string GetFileContent(StringUdi udi);
///
- /// Gets the content of a file as a string.
+ /// Gets the content of a file as a string.
///
/// A file entity identifier.
/// A cancellation token.
- /// A string containing the file content.
- /// Returns null if no content could be read.
+ ///
+ /// A string containing the file content.
+ ///
+ ///
+ /// Returns null if no content could be read.
+ ///
Task GetFileContentAsync(StringUdi udi, CancellationToken token);
///
- /// Gets the length of a file.
+ /// Gets the length of a file.
///
/// A file entity identifier.
- /// The length of the file, or -1 if the file does not exist.
+ ///
+ /// The length of the file, or -1 if the file does not exist.
+ ///
long GetFileLength(StringUdi udi);
///
- /// Gets the length of a file.
+ /// Gets the length of a file.
///
/// A file entity identifier.
/// A cancellation token.
- /// The length of the file, or -1 if the file does not exist.
+ ///
+ /// The length of the file, or -1 if the file does not exist.
+ ///
Task GetFileLengthAsync(StringUdi udi, CancellationToken token);
// TODO (V14): Remove obsolete methods and default implementations for GetFiles and GetFilesAsync overloads.
///
- /// Gets files and store them using a file store.
+ /// Gets files and store them using a file store.
///
/// The UDIs of the files to get.
/// A collection of file types which can store the files.
@@ -72,32 +88,38 @@ public interface IFileSource
void GetFiles(IEnumerable udis, IFileTypeCollection fileTypes);
///
- /// Gets files and store them using a file store.
+ /// Gets files and store them using a file store.
///
/// The UDIs of the files to get.
- /// A collection of file types which can store the files.
/// A flag indicating whether to continue if a file isn't found or to stop and throw a FileNotFoundException.
+ /// A collection of file types which can store the files.
void GetFiles(IEnumerable udis, bool continueOnFileNotFound, IFileTypeCollection fileTypes)
#pragma warning disable CS0618 // Type or member is obsolete
=> GetFiles(udis, fileTypes);
#pragma warning restore CS0618 // Type or member is obsolete
///
- /// Gets files and store them using a file store.
+ /// Gets files and store them using a file store.
///
/// The UDIs of the files to get.
/// A collection of file types which can store the files.
/// A cancellation token.
+ ///
+ /// The task object representing the asynchronous operation.
+ ///
[Obsolete("Please use the method overload taking all parameters. This method overload will be removed in Umbraco 14.")]
Task GetFilesAsync(IEnumerable udis, IFileTypeCollection fileTypes, CancellationToken token);
///
- /// Gets files and store them using a file store.
+ /// Gets files and store them using a file store.
///
/// The UDIs of the files to get.
/// A collection of file types which can store the files.
/// A flag indicating whether to continue if a file isn't found or to stop and throw a FileNotFoundException.
/// A cancellation token.
+ ///
+ /// The task object representing the asynchronous operation.
+ ///
Task GetFilesAsync(IEnumerable udis, IFileTypeCollection fileTypes, bool continueOnFileNotFound, CancellationToken token)
#pragma warning disable CS0618 // Type or member is obsolete
=> GetFilesAsync(udis, fileTypes, token);
diff --git a/src/Umbraco.Core/Deploy/IFileType.cs b/src/Umbraco.Core/Deploy/IFileType.cs
index 466c87a3edba..dfd80e2c1e45 100644
--- a/src/Umbraco.Core/Deploy/IFileType.cs
+++ b/src/Umbraco.Core/Deploy/IFileType.cs
@@ -1,27 +1,101 @@
namespace Umbraco.Cms.Core.Deploy;
+///
+/// Represents a deployable file type.
+///
public interface IFileType
{
+ ///
+ /// Gets a value indicating whether the file can be set using a physical path.
+ ///
+ ///
+ /// true if the file can be set using a physical path; otherwise, false.
+ ///
bool CanSetPhysical { get; }
+ ///
+ /// Gets the stream.
+ ///
+ /// The UDI.
+ ///
+ /// The stream.
+ ///
Stream GetStream(StringUdi udi);
+ ///
+ /// Gets the stream as an asynchronous operation.
+ ///
+ /// The UDI.
+ /// The cancellation token.
+ ///
+ /// The task object representing the asynchronous operation.
+ ///
Task GetStreamAsync(StringUdi udi, CancellationToken token);
+ ///
+ /// Gets the checksum stream.
+ ///
+ /// The UDI.
+ ///
+ /// The checksum stream.
+ ///
Stream GetChecksumStream(StringUdi udi);
+ ///
+ /// Gets the file length in bytes or -1 if not found.
+ ///
+ /// The UDI.
+ ///
+ /// The file length in bytes or -1 if not found.
+ ///
long GetLength(StringUdi udi);
+ ///
+ /// Sets the stream.
+ ///
+ /// The UDI.
+ /// The stream.
void SetStream(StringUdi udi, Stream stream);
+ ///
+ /// Sets the stream as an asynchronous operation.
+ ///
+ /// The UDI.
+ /// The stream.
+ /// The cancellation token.
+ ///
+ /// The task object representing the asynchronous operation.
+ ///
Task SetStreamAsync(StringUdi udi, Stream stream, CancellationToken token);
+ ///
+ /// Sets the physical path of the file.
+ ///
+ /// The UDI.
+ /// The physical path.
+ /// If set to true copies the file instead of moving.
void Set(StringUdi udi, string physicalPath, bool copy = false);
- // this is not pretty as *everywhere* in Deploy we take care of ignoring
- // the physical path and always rely on Core's virtual IFileSystem but
- // Cloud wants to add some of these files to Git and needs the path...
+ ///
+ /// Gets the physical path or if not found.
+ ///
+ /// The UDI.
+ ///
+ /// The physical path or if not found.
+ ///
+ ///
+ /// This is not pretty as *everywhere* in Deploy we take care of ignoring
+ /// the physical path and always rely on the virtual IFileSystem,
+ /// but Cloud wants to add some of these files to Git and needs the path...
+ ///
string GetPhysicalPath(StringUdi udi);
+ ///
+ /// Gets the virtual path or if not found.
+ ///
+ /// The UDI.
+ ///
+ /// The virtual path or if not found.
+ ///
string GetVirtualPath(StringUdi udi);
}
diff --git a/src/Umbraco.Core/Deploy/IFileTypeCollection.cs b/src/Umbraco.Core/Deploy/IFileTypeCollection.cs
index 2ae2bb4bb919..3fc22192b6aa 100644
--- a/src/Umbraco.Core/Deploy/IFileTypeCollection.cs
+++ b/src/Umbraco.Core/Deploy/IFileTypeCollection.cs
@@ -1,8 +1,48 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace Umbraco.Cms.Core.Deploy;
+///
+/// Represents a collection of deployable file types used for each specific entity type.
+///
public interface IFileTypeCollection
{
+ ///
+ /// Gets the for the specified entity type.
+ ///
+ ///
+ /// The .
+ ///
+ /// The entity type.
+ ///
+ /// The for the specified entity type.
+ ///
IFileType this[string entityType] { get; }
+ ///
+ /// Gets the for the specified entity type.
+ ///
+ /// The entity type.
+ /// When this method returns, contains the file type associated with the specified entity type, if the item is found; otherwise, null.
+ ///
+ /// true if the file type associated with the specified entity type was found; otherwise, false.
+ ///
+ bool TryGetValue(string entityType, [NotNullWhen(true)] out IFileType? fileType);
+
+ ///
+ /// Determines whether this collection contains a file type for the specified entity type.
+ ///
+ /// The entity type.
+ ///
+ /// true if this collection contains a file type for the specified entity type; otherwise, false.
+ ///
bool Contains(string entityType);
+
+ ///
+ /// Gets the entity types.
+ ///
+ ///
+ /// The entity types.
+ ///
+ ICollection GetEntityTypes();
}
diff --git a/src/Umbraco.Core/Deploy/IImageSourceParser.cs b/src/Umbraco.Core/Deploy/IImageSourceParser.cs
index cbbaa6bc9ab9..861e15dc1b99 100644
--- a/src/Umbraco.Core/Deploy/IImageSourceParser.cs
+++ b/src/Umbraco.Core/Deploy/IImageSourceParser.cs
@@ -1,49 +1,67 @@
namespace Umbraco.Cms.Core.Deploy;
///
-/// Provides methods to parse image tag sources in property values.
+/// Provides methods to parse image tag sources in property values.
///
public interface IImageSourceParser
{
///
- /// Parses an Umbraco property value and produces an artifact property value.
+ /// Parses an Umbraco property value and produces an artifact property value.
///
/// The property value.
/// A list of dependencies.
- /// The parsed value.
- /// Turns src="/media/..." into src="umb://media/..." and adds the corresponding udi to the dependencies.
+ ///
+ /// The parsed value.
+ ///
+ ///
+ /// Turns src="/media/..." into src="umb://media/..." and adds the corresponding udi to the dependencies.
+ ///
[Obsolete("Please use the overload taking all parameters. This method will be removed in Umbraco 14.")]
string ToArtifact(string value, ICollection dependencies);
///
- /// Parses an Umbraco property value and produces an artifact property value.
+ /// Parses an Umbraco property value and produces an artifact property value.
///
/// The property value.
/// A list of dependencies.
/// The context cache.
- /// The parsed value.
- /// Turns src="/media/..." into src="umb://media/..." and adds the corresponding udi to the dependencies.
+ ///
+ /// The parsed value.
+ ///
+ ///
+ /// Turns src="/media/..." into src="umb://media/..." and adds the corresponding udi to the dependencies.
+ ///
+ string ToArtifact(string value, ICollection dependencies, IContextCache contextCache)
#pragma warning disable CS0618 // Type or member is obsolete
- string ToArtifact(string value, ICollection dependencies, IContextCache contextCache) => ToArtifact(value, dependencies);
+ => ToArtifact(value, dependencies);
#pragma warning restore CS0618 // Type or member is obsolete
///
- /// Parses an artifact property value and produces an Umbraco property value.
+ /// Parses an artifact property value and produces an Umbraco property value.
///
/// The artifact property value.
- /// The parsed value.
- /// Turns umb://media/... into /media/....
+ ///
+ /// The parsed value.
+ ///
+ ///
+ /// Turns umb://media/... into /media/....
+ ///
[Obsolete("Please use the overload taking all parameters. This method will be removed in Umbraco 14.")]
string FromArtifact(string value);
///
- /// Parses an artifact property value and produces an Umbraco property value.
+ /// Parses an artifact property value and produces an Umbraco property value.
///
/// The artifact property value.
/// The context cache.
- /// The parsed value.
- /// Turns umb://media/... into /media/....
+ ///
+ /// The parsed value.
+ ///
+ ///
+ /// Turns umb://media/... into /media/....
+ ///
+ string FromArtifact(string value, IContextCache contextCache)
#pragma warning disable CS0618 // Type or member is obsolete
- string FromArtifact(string value, IContextCache contextCache) => FromArtifact(value);
+ => FromArtifact(value);
#pragma warning restore CS0618 // Type or member is obsolete
}
diff --git a/src/Umbraco.Core/Deploy/ILocalLinkParser.cs b/src/Umbraco.Core/Deploy/ILocalLinkParser.cs
index d84bf35af199..9273faa05483 100644
--- a/src/Umbraco.Core/Deploy/ILocalLinkParser.cs
+++ b/src/Umbraco.Core/Deploy/ILocalLinkParser.cs
@@ -1,55 +1,69 @@
namespace Umbraco.Cms.Core.Deploy;
///
-/// Provides methods to parse local link tags in property values.
+/// Provides methods to parse local link tags in property values.
///
public interface ILocalLinkParser
{
///
- /// Parses an Umbraco property value and produces an artifact property value.
+ /// Parses an Umbraco property value and produces an artifact property value.
///
/// The property value.
/// A list of dependencies.
- /// The parsed value.
+ ///
+ /// The parsed value.
+ ///
///
- /// Turns {{localLink:1234}} into {{localLink:umb://{type}/{id}}} and adds the corresponding udi to the
- /// dependencies.
+ /// Turns {{localLink:1234}} into {{localLink:umb://{type}/{id}}} and adds the corresponding udi to the
+ /// dependencies.
///
[Obsolete("Please use the overload taking all parameters. This method will be removed in Umbraco 14.")]
string ToArtifact(string value, ICollection dependencies);
///
- /// Parses an Umbraco property value and produces an artifact property value.
+ /// Parses an Umbraco property value and produces an artifact property value.
///
/// The property value.
/// A list of dependencies.
/// The context cache.
- /// The parsed value.
+ ///
+ /// The parsed value.
+ ///
///
- /// Turns {{localLink:1234}} into {{localLink:umb://{type}/{id}}} and adds the corresponding udi to the
- /// dependencies.
+ /// Turns {{localLink:1234}} into {{localLink:umb://{type}/{id}}} and adds the corresponding udi to the
+ /// dependencies.
///
+ string ToArtifact(string value, ICollection dependencies, IContextCache contextCache)
#pragma warning disable CS0618 // Type or member is obsolete
- string ToArtifact(string value, ICollection dependencies, IContextCache contextCache) => ToArtifact(value, dependencies);
+ => ToArtifact(value, dependencies);
#pragma warning restore CS0618 // Type or member is obsolete
///
- /// Parses an artifact property value and produces an Umbraco property value.
+ /// Parses an artifact property value and produces an Umbraco property value.
///
/// The artifact property value.
- /// The parsed value.
- /// Turns {{localLink:umb://{type}/{id}}} into {{localLink:1234}}.
+ ///
+ /// The parsed value.
+ ///
+ ///
+ /// Turns {{localLink:umb://{type}/{id}}} into {{localLink:1234}}.
+ ///
[Obsolete("Please use the overload taking all parameters. This method will be removed in Umbraco 14.")]
string FromArtifact(string value);
///
- /// Parses an artifact property value and produces an Umbraco property value.
+ /// Parses an artifact property value and produces an Umbraco property value.
///
/// The artifact property value.
/// The context cache.
- /// The parsed value.
- /// Turns {{localLink:umb://{type}/{id}}} into {{localLink:1234}}.
+ ///
+ /// The parsed value.
+ ///
+ ///
+ /// Turns {{localLink:umb://{type}/{id}}} into {{localLink:1234}}.
+ ///
+ string FromArtifact(string value, IContextCache contextCache)
#pragma warning disable CS0618 // Type or member is obsolete
- string FromArtifact(string value, IContextCache contextCache) => FromArtifact(value);
+ => FromArtifact(value);
#pragma warning restore CS0618 // Type or member is obsolete
}
diff --git a/src/Umbraco.Core/Deploy/IMacroParser.cs b/src/Umbraco.Core/Deploy/IMacroParser.cs
index 17f06992d52a..de14bb528fa6 100644
--- a/src/Umbraco.Core/Deploy/IMacroParser.cs
+++ b/src/Umbraco.Core/Deploy/IMacroParser.cs
@@ -1,65 +1,82 @@
namespace Umbraco.Cms.Core.Deploy;
+///
+/// Provides methods to parse macro tags in property values.
+///
public interface IMacroParser
{
///
- /// Parses an Umbraco property value and produces an artifact property value.
+ /// Parses an Umbraco property value and produces an artifact property value.
///
/// Property value.
/// A list of dependencies.
- /// Parsed value.
+ ///
+ /// Parsed value.
+ ///
[Obsolete("Please use the overload taking all parameters. This method will be removed in Umbraco 14.")]
string ToArtifact(string value, ICollection dependencies);
///
- /// Parses an Umbraco property value and produces an artifact property value.
+ /// Parses an Umbraco property value and produces an artifact property value.
///
/// Property value.
/// A list of dependencies.
/// The context cache.
- /// Parsed value.
+ ///
+ /// Parsed value.
+ ///
+ string ToArtifact(string value, ICollection dependencies, IContextCache contextCache)
#pragma warning disable CS0618 // Type or member is obsolete
- string ToArtifact(string value, ICollection dependencies, IContextCache contextCache) => ToArtifact(value, dependencies);
+ => ToArtifact(value, dependencies);
#pragma warning restore CS0618 // Type or member is obsolete
///
- /// Parses an artifact property value and produces an Umbraco property value.
+ /// Parses an artifact property value and produces an Umbraco property value.
///
/// Artifact property value.
- /// Parsed value.
+ ///
+ /// Parsed value.
+ ///
[Obsolete("Please use the overload taking all parameters. This method will be removed in Umbraco 14.")]
string FromArtifact(string value);
///
- /// Parses an artifact property value and produces an Umbraco property value.
+ /// Parses an artifact property value and produces an Umbraco property value.
///
/// Artifact property value.
/// The context cache.
- /// Parsed value.
+ ///
+ /// Parsed value.
+ ///
+ string FromArtifact(string value, IContextCache contextCache)
#pragma warning disable CS0618 // Type or member is obsolete
- string FromArtifact(string value, IContextCache contextCache) => FromArtifact(value);
+ => FromArtifact(value);
#pragma warning restore CS0618 // Type or member is obsolete
///
- /// Tries to replace the value of the attribute/parameter with a value containing a converted identifier.
+ /// Tries to replace the value of the attribute/parameter with a value containing a converted identifier.
///
/// Value to attempt to convert
/// Alias of the editor used for the parameter
/// Collection to add dependencies to when performing ToArtifact
/// Indicates which action is being performed (to or from artifact)
- /// Value with converted identifiers
+ ///
+ /// Value with converted identifiers
+ ///
[Obsolete("Please use the overload taking all parameters. This method will be removed in Umbraco 14.")]
string ReplaceAttributeValue(string value, string editorAlias, ICollection dependencies, Direction direction);
///
- /// Tries to replace the value of the attribute/parameter with a value containing a converted identifier.
+ /// Tries to replace the value of the attribute/parameter with a value containing a converted identifier.
///
/// Value to attempt to convert
/// Alias of the editor used for the parameter
/// Collection to add dependencies to when performing ToArtifact
/// Indicates which action is being performed (to or from artifact)
/// The context cache.
- /// Value with converted identifiers
+ ///
+ /// Value with converted identifiers
+ ///
string ReplaceAttributeValue(string value, string editorAlias, ICollection dependencies, Direction direction, IContextCache contextCache)
#pragma warning disable CS0618 // Type or member is obsolete
=> ReplaceAttributeValue(value, editorAlias, dependencies, direction);
diff --git a/src/Umbraco.Core/Deploy/IServiceConnector.cs b/src/Umbraco.Core/Deploy/IServiceConnector.cs
index 84617943c6d3..e831ed9e082e 100644
--- a/src/Umbraco.Core/Deploy/IServiceConnector.cs
+++ b/src/Umbraco.Core/Deploy/IServiceConnector.cs
@@ -5,7 +5,6 @@ namespace Umbraco.Cms.Core.Deploy;
///
/// Connects to an Umbraco service.
///
-///
public interface IServiceConnector : IDiscoverable
{
///
diff --git a/src/Umbraco.Core/Deploy/IUniqueIdentifyingServiceConnector.cs b/src/Umbraco.Core/Deploy/IUniqueIdentifyingServiceConnector.cs
index c68906bbbf1d..3178328e033e 100644
--- a/src/Umbraco.Core/Deploy/IUniqueIdentifyingServiceConnector.cs
+++ b/src/Umbraco.Core/Deploy/IUniqueIdentifyingServiceConnector.cs
@@ -1,24 +1,26 @@
namespace Umbraco.Cms.Core.Deploy;
///
-/// Provides a method to retrieve an artifact's unique identifier.
+/// Provides a method to retrieve an artifact's unique identifier.
///
///
-/// Artifacts are uniquely identified by their , however they represent
-/// elements in Umbraco that may be uniquely identified by another value. For example,
-/// a content type is uniquely identified by its alias. If someone creates a new content
-/// type, and tries to deploy it to a remote environment where a content type with the
-/// same alias already exists, both content types end up having different
-/// but the same alias. By default, Deploy would fail and throw when trying to save the
-/// new content type (duplicate alias). However, if the connector also implements this
-/// interface, the situation can be detected beforehand and reported in a nicer way.
+/// Artifacts are uniquely identified by their , however they represent
+/// elements in Umbraco that may be uniquely identified by another value. For example,
+/// a content type is uniquely identified by its alias. If someone creates a new content
+/// type, and tries to deploy it to a remote environment where a content type with the
+/// same alias already exists, both content types end up having different
+/// but the same alias. By default, Deploy would fail and throw when trying to save the
+/// new content type (duplicate alias). However, if the connector also implements this
+/// interface, the situation can be detected beforehand and reported in a nicer way.
///
public interface IUniqueIdentifyingServiceConnector
{
///
- /// Gets the unique identifier of the specified artifact.
+ /// Gets the unique identifier of the specified artifact.
///
/// The artifact.
- /// The unique identifier.
+ ///
+ /// The unique identifier.
+ ///
string GetUniqueIdentifier(IArtifact artifact);
}
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
index 572fe05dd4bc..00cb113ddf81 100644
--- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
+++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
@@ -1941,6 +1941,14 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Create headerLogsNo webhook headers have been added
+ No events were found.
+ Enabled
+ Events
+ Event
+ Url
+ Types
+ Webhook key
+ Retry countAdd language
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml
index 33901735eec1..c6c028099742 100644
--- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml
+++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml
@@ -2023,6 +2023,14 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Create headerLogsNo webhook headers have been added
+ No events were found.
+ Enabled
+ Events
+ Event
+ Url
+ Types
+ Webhook key
+ Retry countAdd language
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml b/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml
index b30dba4a6910..24da7d0066fa 100644
--- a/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml
+++ b/src/Umbraco.Core/EmbeddedResources/Lang/fr.xml
@@ -1585,6 +1585,13 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à
Ceci n'est pas d'application pour un Type d'ElémentVous avez apporté des modifications à cette propriété. Etes-vous certain.e de vouloir les annuler?
+
+ Créer un webhook
+ Ajouter un header au webhook
+ Logs
+ Ajouter un Type de Document
+ Ajouter un Type de Media
+
Ajouter une langueLangue obligatoire
@@ -1715,6 +1722,7 @@ Pour gérer votre site, ouvrez simplement le backoffice Umbraco et commencez à
ConfigurationModélisationParties Tierces
+ WebhooksNouvelle mise à jour disponible
diff --git a/src/Umbraco.Core/HealthChecks/HealthCheckResults.cs b/src/Umbraco.Core/HealthChecks/HealthCheckResults.cs
index afeb8ba9fa3b..f88b96c9d662 100644
--- a/src/Umbraco.Core/HealthChecks/HealthCheckResults.cs
+++ b/src/Umbraco.Core/HealthChecks/HealthCheckResults.cs
@@ -53,6 +53,8 @@ public static async Task Create(IEnumerable che
return new HealthCheckResults(results, allChecksSuccessful);
}
+ public static async Task Create(HealthCheck check) => await Create(new List() { check });
+
public void LogResults()
{
Logger.LogInformation("Scheduled health check results:");
diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs
index 4cf5bdd6af87..9bced05ff5a0 100644
--- a/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedPropertyType.cs
@@ -71,6 +71,14 @@ public interface IPublishedPropertyType
///
Type ModelClrType { get; }
+ ///
+ /// Gets the property model Delivery Api CLR type.
+ ///
+ ///
+ /// The model CLR type may be a type, or may contain types.
+ ///
+ Type DeliveryApiModelClrType => ModelClrType;
+
///
/// Gets the property CLR type.
///
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs
index 52e33717673e..ed2d19cd6fc8 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs
@@ -24,6 +24,7 @@ public class PublishedPropertyType : IPublishedPropertyType
private PropertyCacheLevel _deliveryApiCacheLevelForExpansion;
private Type? _modelClrType;
+ private Type? _deliveryApiModelClrType;
private Type? _clrType;
#region Constructors
@@ -192,17 +193,13 @@ private void InitializeLocked()
}
}
+ var deliveryApiPropertyValueConverter = _converter as IDeliveryApiPropertyValueConverter;
+
_cacheLevel = _converter?.GetPropertyCacheLevel(this) ?? PropertyCacheLevel.Snapshot;
- if (_converter is IDeliveryApiPropertyValueConverter deliveryApiPropertyValueConverter)
- {
- _deliveryApiCacheLevel = deliveryApiPropertyValueConverter.GetDeliveryApiPropertyCacheLevel(this);
- _deliveryApiCacheLevelForExpansion = deliveryApiPropertyValueConverter.GetDeliveryApiPropertyCacheLevelForExpansion(this);
- }
- else
- {
- _deliveryApiCacheLevel = _deliveryApiCacheLevelForExpansion = _cacheLevel;
- }
+ _deliveryApiCacheLevel = deliveryApiPropertyValueConverter?.GetDeliveryApiPropertyCacheLevel(this) ?? _cacheLevel;
+ _deliveryApiCacheLevelForExpansion = deliveryApiPropertyValueConverter?.GetDeliveryApiPropertyCacheLevelForExpansion(this) ?? _cacheLevel;
_modelClrType = _converter?.GetPropertyValueType(this) ?? typeof(object);
+ _deliveryApiModelClrType = deliveryApiPropertyValueConverter?.GetDeliveryApiPropertyValueType(this) ?? _modelClrType;
}
///
@@ -352,6 +349,20 @@ public Type ModelClrType
}
}
+ ///
+ public Type DeliveryApiModelClrType
+ {
+ get
+ {
+ if (!_initialized)
+ {
+ Initialize();
+ }
+
+ return _deliveryApiModelClrType!;
+ }
+ }
+
///
public Type? ClrType
{
diff --git a/src/Umbraco.Core/Notifications/HealthCheckCompletedNotification.cs b/src/Umbraco.Core/Notifications/HealthCheckCompletedNotification.cs
new file mode 100644
index 000000000000..67df86deb169
--- /dev/null
+++ b/src/Umbraco.Core/Notifications/HealthCheckCompletedNotification.cs
@@ -0,0 +1,13 @@
+using Umbraco.Cms.Core.HealthChecks;
+
+namespace Umbraco.Cms.Core.Notifications;
+
+public class HealthCheckCompletedNotification : INotification
+{
+ public HealthCheckCompletedNotification(HealthCheckResults healthCheckResults)
+ {
+ HealthCheckResults = healthCheckResults;
+ }
+
+ public HealthCheckResults HealthCheckResults { get; }
+}
diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotAccessor.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotAccessor.cs
index 0f9cc8fca9ba..8abc0906cedc 100644
--- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotAccessor.cs
+++ b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotAccessor.cs
@@ -1,3 +1,5 @@
+using System.Diagnostics.CodeAnalysis;
+
namespace Umbraco.Cms.Core.PublishedCache;
///
@@ -6,5 +8,5 @@ namespace Umbraco.Cms.Core.PublishedCache;
///
public interface IPublishedSnapshotAccessor
{
- bool TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot);
+ bool TryGetPublishedSnapshot([NotNullWhen(true)] out IPublishedSnapshot? publishedSnapshot);
}
diff --git a/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs b/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs
index 8f3e4fe8271b..91e32e6db4e8 100644
--- a/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs
+++ b/src/Umbraco.Core/PublishedCache/UmbracoContextPublishedSnapshotAccessor.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
using Umbraco.Cms.Core.Web;
namespace Umbraco.Cms.Core.PublishedCache;
@@ -29,7 +30,7 @@ public IPublishedSnapshot? PublishedSnapshot
set => throw new NotSupportedException(); // not ok to set
}
- public bool TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot)
+ public bool TryGetPublishedSnapshot([NotNullWhen(true)] out IPublishedSnapshot? publishedSnapshot)
{
if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext))
{
diff --git a/src/Umbraco.Core/Webhooks/Events/Content/ContentCopiedWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/Content/ContentCopiedWebhookEvent.cs
new file mode 100644
index 000000000000..3d6f0163d70b
--- /dev/null
+++ b/src/Umbraco.Core/Webhooks/Events/Content/ContentCopiedWebhookEvent.cs
@@ -0,0 +1,37 @@
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+
+namespace Umbraco.Cms.Core.Webhooks.Events.Content;
+
+[WebhookEvent("Content Copied", Constants.WebhookEvents.Types.Content)]
+public class ContentCopiedWebhookEvent : WebhookEventBase
+{
+ public ContentCopiedWebhookEvent(
+ IWebhookFiringService webhookFiringService,
+ IWebhookService webhookService,
+ IOptionsMonitor webhookSettings,
+ IServerRoleAccessor serverRoleAccessor)
+ : base(
+ webhookFiringService,
+ webhookService,
+ webhookSettings,
+ serverRoleAccessor)
+ {
+ }
+
+ public override string Alias => Constants.WebhookEvents.Aliases.ContentCopied;
+
+ public override object? ConvertNotificationToRequestPayload(ContentCopiedNotification notification)
+ {
+ return new
+ {
+ notification.Copy,
+ notification.Original,
+ notification.ParentId,
+ notification.RelateToOriginal
+ };
+ }
+}
diff --git a/src/Umbraco.Core/Webhooks/Events/Content/ContentDeletedBlueprintWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/Content/ContentDeletedBlueprintWebhookEvent.cs
new file mode 100644
index 000000000000..996b8abd15d9
--- /dev/null
+++ b/src/Umbraco.Core/Webhooks/Events/Content/ContentDeletedBlueprintWebhookEvent.cs
@@ -0,0 +1,32 @@
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+
+namespace Umbraco.Cms.Core.Webhooks.Events.Content;
+
+[WebhookEvent("Content Template [Blueprint] Deleted", Constants.WebhookEvents.Types.Content)]
+public class ContentDeletedBlueprintWebhookEvent : WebhookEventContentBase
+{
+ public ContentDeletedBlueprintWebhookEvent(
+ IWebhookFiringService webhookFiringService,
+ IWebhookService webhookService,
+ IOptionsMonitor webhookSettings,
+ IServerRoleAccessor serverRoleAccessor)
+ : base(
+ webhookFiringService,
+ webhookService,
+ webhookSettings,
+ serverRoleAccessor)
+ {
+ }
+
+ public override string Alias => Constants.WebhookEvents.Aliases.ContentDeletedBlueprint;
+
+ protected override IEnumerable GetEntitiesFromNotification(ContentDeletedBlueprintNotification notification) =>
+ notification.DeletedBlueprints;
+
+ protected override object ConvertEntityToRequestPayload(IContent entity) => entity;
+}
diff --git a/src/Umbraco.Core/Webhooks/Events/Content/ContentDeletedVersionsWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/Content/ContentDeletedVersionsWebhookEvent.cs
new file mode 100644
index 000000000000..c543bfe3cad7
--- /dev/null
+++ b/src/Umbraco.Core/Webhooks/Events/Content/ContentDeletedVersionsWebhookEvent.cs
@@ -0,0 +1,37 @@
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+
+namespace Umbraco.Cms.Core.Webhooks.Events.Content;
+
+[WebhookEvent("Content Versions Deleted", Constants.WebhookEvents.Types.Content)]
+public class ContentDeletedVersionsWebhookEvent : WebhookEventBase
+{
+ public ContentDeletedVersionsWebhookEvent(
+ IWebhookFiringService webhookFiringService,
+ IWebhookService webhookService,
+ IOptionsMonitor webhookSettings,
+ IServerRoleAccessor serverRoleAccessor)
+ : base(
+ webhookFiringService,
+ webhookService,
+ webhookSettings,
+ serverRoleAccessor)
+ {
+ }
+
+ public override string Alias => Constants.WebhookEvents.Aliases.ContentDeletedVersions;
+
+ public override object? ConvertNotificationToRequestPayload(ContentDeletedVersionsNotification notification)
+ {
+ return new
+ {
+ notification.Id,
+ notification.DeletePriorVersions,
+ notification.SpecificVersion,
+ notification.DateToRetain
+ };
+ }
+}
diff --git a/src/Umbraco.Core/Webhooks/Events/ContentDeleteWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/Content/ContentDeletedWebhookEvent.cs
similarity index 77%
rename from src/Umbraco.Core/Webhooks/Events/ContentDeleteWebhookEvent.cs
rename to src/Umbraco.Core/Webhooks/Events/Content/ContentDeletedWebhookEvent.cs
index 85a1f39ba9d4..128282299566 100644
--- a/src/Umbraco.Core/Webhooks/Events/ContentDeleteWebhookEvent.cs
+++ b/src/Umbraco.Core/Webhooks/Events/Content/ContentDeletedWebhookEvent.cs
@@ -5,12 +5,12 @@
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
-namespace Umbraco.Cms.Core.Webhooks.Events;
+namespace Umbraco.Cms.Core.Webhooks.Events.Content;
-[WebhookEvent("Content was deleted", Constants.WebhookEvents.Types.Content)]
-public class ContentDeleteWebhookEvent : WebhookEventContentBase
+[WebhookEvent("Content Deleted", Constants.WebhookEvents.Types.Content)]
+public class ContentDeletedWebhookEvent : WebhookEventContentBase
{
- public ContentDeleteWebhookEvent(
+ public ContentDeletedWebhookEvent(
IWebhookFiringService webhookFiringService,
IWebhookService webhookService,
IOptionsMonitor webhookSettings,
diff --git a/src/Umbraco.Core/Webhooks/Events/Content/ContentEmptiedRecycleBinWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/Content/ContentEmptiedRecycleBinWebhookEvent.cs
new file mode 100644
index 000000000000..8670d23c49e5
--- /dev/null
+++ b/src/Umbraco.Core/Webhooks/Events/Content/ContentEmptiedRecycleBinWebhookEvent.cs
@@ -0,0 +1,41 @@
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.DeliveryApi;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.PublishedCache;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+
+namespace Umbraco.Cms.Core.Webhooks.Events.Content;
+
+[WebhookEvent("Content Recycle Bin Emptied", Constants.WebhookEvents.Types.Content)]
+public class ContentEmptiedRecycleBinWebhookEvent : WebhookEventContentBase
+{
+ private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
+ private readonly IApiContentBuilder _apiContentBuilder;
+
+ public ContentEmptiedRecycleBinWebhookEvent(
+ IWebhookFiringService webhookFiringService,
+ IWebhookService webhookService,
+ IOptionsMonitor webhookSettings,
+ IServerRoleAccessor serverRoleAccessor,
+ IPublishedSnapshotAccessor publishedSnapshotAccessor,
+ IApiContentBuilder apiContentBuilder)
+ : base(
+ webhookFiringService,
+ webhookService,
+ webhookSettings,
+ serverRoleAccessor)
+ {
+ _publishedSnapshotAccessor = publishedSnapshotAccessor;
+ _apiContentBuilder = apiContentBuilder;
+ }
+
+ public override string Alias => Constants.WebhookEvents.Aliases.ContentEmptiedRecycleBin;
+
+ protected override IEnumerable GetEntitiesFromNotification(ContentEmptiedRecycleBinNotification notification) =>
+ notification.DeletedEntities;
+
+ protected override object? ConvertEntityToRequestPayload(IContent entity) => entity;
+}
diff --git a/src/Umbraco.Core/Webhooks/Events/Content/ContentMovedToRecycleBinWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/Content/ContentMovedToRecycleBinWebhookEvent.cs
new file mode 100644
index 000000000000..6ed74d8c66e4
--- /dev/null
+++ b/src/Umbraco.Core/Webhooks/Events/Content/ContentMovedToRecycleBinWebhookEvent.cs
@@ -0,0 +1,29 @@
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+
+namespace Umbraco.Cms.Core.Webhooks.Events.Content;
+
+[WebhookEvent("Content Moved to Recycle Bin", Constants.WebhookEvents.Types.Content)]
+public class ContentMovedToRecycleBinWebhookEvent : WebhookEventBase
+{
+ public ContentMovedToRecycleBinWebhookEvent(
+ IWebhookFiringService webhookFiringService,
+ IWebhookService webhookService,
+ IOptionsMonitor webhookSettings,
+ IServerRoleAccessor serverRoleAccessor)
+ : base(
+ webhookFiringService,
+ webhookService,
+ webhookSettings,
+ serverRoleAccessor)
+ {
+ }
+
+ public override string Alias => Constants.WebhookEvents.Aliases.ContentMovedToRecycleBin;
+
+ public override object? ConvertNotificationToRequestPayload(ContentMovedToRecycleBinNotification notification)
+ => notification.MoveInfoCollection;
+}
diff --git a/src/Umbraco.Core/Webhooks/Events/Content/ContentMovedWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/Content/ContentMovedWebhookEvent.cs
new file mode 100644
index 000000000000..46d487c499e0
--- /dev/null
+++ b/src/Umbraco.Core/Webhooks/Events/Content/ContentMovedWebhookEvent.cs
@@ -0,0 +1,29 @@
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+
+namespace Umbraco.Cms.Core.Webhooks.Events.Content;
+
+[WebhookEvent("Content Moved", Constants.WebhookEvents.Types.Content)]
+public class ContentMovedWebhookEvent : WebhookEventBase
+{
+ public ContentMovedWebhookEvent(
+ IWebhookFiringService webhookFiringService,
+ IWebhookService webhookService,
+ IOptionsMonitor webhookSettings,
+ IServerRoleAccessor serverRoleAccessor)
+ : base(
+ webhookFiringService,
+ webhookService,
+ webhookSettings,
+ serverRoleAccessor)
+ {
+ }
+
+ public override string Alias => Constants.WebhookEvents.Aliases.ContentMoved;
+
+ public override object? ConvertNotificationToRequestPayload(ContentMovedNotification notification)
+ => notification.MoveInfoCollection;
+}
diff --git a/src/Umbraco.Core/Webhooks/Events/ContentPublishWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/Content/ContentPublishedWebhookEvent.cs
similarity index 86%
rename from src/Umbraco.Core/Webhooks/Events/ContentPublishWebhookEvent.cs
rename to src/Umbraco.Core/Webhooks/Events/Content/ContentPublishedWebhookEvent.cs
index e1ba7125ec78..5847fb450e6c 100644
--- a/src/Umbraco.Core/Webhooks/Events/ContentPublishWebhookEvent.cs
+++ b/src/Umbraco.Core/Webhooks/Events/Content/ContentPublishedWebhookEvent.cs
@@ -8,15 +8,15 @@
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
-namespace Umbraco.Cms.Core.Webhooks.Events;
+namespace Umbraco.Cms.Core.Webhooks.Events.Content;
-[WebhookEvent("Content was published", Constants.WebhookEvents.Types.Content)]
-public class ContentPublishWebhookEvent : WebhookEventContentBase
+[WebhookEvent("Content Published", Constants.WebhookEvents.Types.Content)]
+public class ContentPublishedWebhookEvent : WebhookEventContentBase
{
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
private readonly IApiContentBuilder _apiContentBuilder;
- public ContentPublishWebhookEvent(
+ public ContentPublishedWebhookEvent(
IWebhookFiringService webhookFiringService,
IWebhookService webhookService,
IOptionsMonitor webhookSettings,
diff --git a/src/Umbraco.Core/Webhooks/Events/Content/ContentRolledBack.cs b/src/Umbraco.Core/Webhooks/Events/Content/ContentRolledBack.cs
new file mode 100644
index 000000000000..f38ff571f908
--- /dev/null
+++ b/src/Umbraco.Core/Webhooks/Events/Content/ContentRolledBack.cs
@@ -0,0 +1,52 @@
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.DeliveryApi;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.PublishedCache;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+
+namespace Umbraco.Cms.Core.Webhooks.Events.Content;
+
+[WebhookEvent("Content Rolled Back", Constants.WebhookEvents.Types.Content)]
+public class ContentRolledBackWebhookEvent : WebhookEventContentBase
+{
+ private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
+ private readonly IApiContentBuilder _apiContentBuilder;
+
+ public ContentRolledBackWebhookEvent(
+ IWebhookFiringService webhookFiringService,
+ IWebhookService webhookService,
+ IOptionsMonitor webhookSettings,
+ IServerRoleAccessor serverRoleAccessor,
+ IPublishedSnapshotAccessor publishedSnapshotAccessor,
+ IApiContentBuilder apiContentBuilder)
+ : base(
+ webhookFiringService,
+ webhookService,
+ webhookSettings,
+ serverRoleAccessor)
+ {
+ _publishedSnapshotAccessor = publishedSnapshotAccessor;
+ _apiContentBuilder = apiContentBuilder;
+ }
+
+ public override string Alias => Constants.WebhookEvents.Aliases.ContentRolledBack;
+
+ protected override IEnumerable GetEntitiesFromNotification(ContentRolledBackNotification notification) =>
+ new List { notification.Entity };
+
+ protected override object? ConvertEntityToRequestPayload(IContent entity)
+ {
+ if (_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) is false || publishedSnapshot!.Content is null)
+ {
+ return null;
+ }
+
+ // Get preview/saved version of content for a rollback
+ IPublishedContent? publishedContent = publishedSnapshot.Content.GetById(true, entity.Key);
+ return publishedContent is null ? null : _apiContentBuilder.Build(publishedContent);
+ }
+}
diff --git a/src/Umbraco.Core/Webhooks/Events/Content/ContentSavedBlueprintWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/Content/ContentSavedBlueprintWebhookEvent.cs
new file mode 100644
index 000000000000..40630b453f39
--- /dev/null
+++ b/src/Umbraco.Core/Webhooks/Events/Content/ContentSavedBlueprintWebhookEvent.cs
@@ -0,0 +1,33 @@
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+
+namespace Umbraco.Cms.Core.Webhooks.Events.Content;
+
+[WebhookEvent("Content Template [Blueprint] Saved", Constants.WebhookEvents.Types.Content)]
+public class ContentSavedBlueprintWebhookEvent : WebhookEventContentBase
+{
+ public ContentSavedBlueprintWebhookEvent(
+ IWebhookFiringService webhookFiringService,
+ IWebhookService webhookService,
+ IOptionsMonitor webhookSettings,
+ IServerRoleAccessor serverRoleAccessor)
+ : base(
+ webhookFiringService,
+ webhookService,
+ webhookSettings,
+ serverRoleAccessor)
+ {
+ }
+
+ public override string Alias => Constants.WebhookEvents.Aliases.ContentSavedBlueprint;
+
+ protected override IEnumerable
+ GetEntitiesFromNotification(ContentSavedBlueprintNotification notification)
+ => new List { notification.SavedBlueprint };
+
+ protected override object ConvertEntityToRequestPayload(IContent entity) => entity;
+}
diff --git a/src/Umbraco.Core/Webhooks/Events/Content/ContentSavedWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/Content/ContentSavedWebhookEvent.cs
new file mode 100644
index 000000000000..fac16de822e5
--- /dev/null
+++ b/src/Umbraco.Core/Webhooks/Events/Content/ContentSavedWebhookEvent.cs
@@ -0,0 +1,52 @@
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.DeliveryApi;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.PublishedCache;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+
+namespace Umbraco.Cms.Core.Webhooks.Events.Content;
+
+[WebhookEvent("Content Saved", Constants.WebhookEvents.Types.Content)]
+public class ContentSavedWebhookEvent : WebhookEventContentBase
+{
+ private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
+ private readonly IApiContentBuilder _apiContentBuilder;
+
+ public ContentSavedWebhookEvent(
+ IWebhookFiringService webhookFiringService,
+ IWebhookService webhookService,
+ IOptionsMonitor webhookSettings,
+ IServerRoleAccessor serverRoleAccessor,
+ IPublishedSnapshotAccessor publishedSnapshotAccessor,
+ IApiContentBuilder apiContentBuilder)
+ : base(
+ webhookFiringService,
+ webhookService,
+ webhookSettings,
+ serverRoleAccessor)
+ {
+ _publishedSnapshotAccessor = publishedSnapshotAccessor;
+ _apiContentBuilder = apiContentBuilder;
+ }
+
+ public override string Alias => Constants.WebhookEvents.Aliases.ContentSaved;
+
+ protected override IEnumerable GetEntitiesFromNotification(ContentSavedNotification notification) =>
+ notification.SavedEntities;
+
+ protected override object? ConvertEntityToRequestPayload(IContent entity)
+ {
+ if (_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) is false || publishedSnapshot!.Content is null)
+ {
+ return null;
+ }
+
+ // Get preview/saved version of content
+ IPublishedContent? publishedContent = publishedSnapshot.Content.GetById(true, entity.Key);
+ return publishedContent is null ? null : _apiContentBuilder.Build(publishedContent);
+ }
+}
diff --git a/src/Umbraco.Core/Webhooks/Events/Content/ContentSortedWebhookEvent.cs b/src/Umbraco.Core/Webhooks/Events/Content/ContentSortedWebhookEvent.cs
new file mode 100644
index 000000000000..d2a95028e2fd
--- /dev/null
+++ b/src/Umbraco.Core/Webhooks/Events/Content/ContentSortedWebhookEvent.cs
@@ -0,0 +1,53 @@
+using Microsoft.Extensions.Options;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.DeliveryApi;
+using Umbraco.Cms.Core.Models;
+using Umbraco.Cms.Core.Models.PublishedContent;
+using Umbraco.Cms.Core.Notifications;
+using Umbraco.Cms.Core.PublishedCache;
+using Umbraco.Cms.Core.Services;
+using Umbraco.Cms.Core.Sync;
+
+namespace Umbraco.Cms.Core.Webhooks.Events.Content;
+
+[WebhookEvent("Content Sorted", Constants.WebhookEvents.Types.Content)]
+public class ContentSortedWebhookEvent : WebhookEventBase
+{
+ private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
+ private readonly IApiContentBuilder _apiContentBuilder;
+
+ public ContentSortedWebhookEvent(
+ IWebhookFiringService webhookFiringService,
+ IWebhookService webhookService,
+ IOptionsMonitor webhookSettings,
+ IServerRoleAccessor serverRoleAccessor,
+ IPublishedSnapshotAccessor publishedSnapshotAccessor,
+ IApiContentBuilder apiContentBuilder)
+ : base(
+ webhookFiringService,
+ webhookService,
+ webhookSettings,
+ serverRoleAccessor)
+ {
+ _publishedSnapshotAccessor = publishedSnapshotAccessor;
+ _apiContentBuilder = apiContentBuilder;
+ }
+
+ public override string Alias => Constants.WebhookEvents.Aliases.ContentSorted;
+
+ public override object? ConvertNotificationToRequestPayload(ContentSortedNotification notification)
+ {
+ if (_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) is false || publishedSnapshot!.Content is null)
+ {
+ return null;
+ }
+ var sortedEntities = new List