Skip to content

Commit 21520ee

Browse files
MigaroezAndyButland
authored andcommitted
Webhooks: Register OutputExpansionStrategy for webhooks if Delivery API is not enabled (closes #20272 for 13) (#20721)
V13 implementation off #20559 (Webhooks: Register OutputExpansionStrategy for webhooks if Delivery API is not enabled)
1 parent f7b874d commit 21520ee

File tree

6 files changed

+211
-154
lines changed

6 files changed

+211
-154
lines changed

src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,26 +34,33 @@ public static IUmbracoBuilder AddDeliveryApi(this IUmbracoBuilder builder)
3434
builder.Services.AddScoped<IRequestStartItemProvider, RequestStartItemProvider>();
3535
builder.Services.AddScoped<RequestContextOutputExpansionStrategy>();
3636
builder.Services.AddScoped<RequestContextOutputExpansionStrategyV2>();
37-
builder.Services.AddScoped<IOutputExpansionStrategy>(provider =>
38-
{
39-
HttpContext? httpContext = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
40-
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
41-
if (apiVersion is null)
37+
38+
builder.Services.AddUnique<IOutputExpansionStrategy>(
39+
provider =>
4240
{
43-
return provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
44-
}
41+
HttpContext? httpContext = provider.GetRequiredService<IHttpContextAccessor>().HttpContext;
42+
ApiVersion? apiVersion = httpContext?.GetRequestedApiVersion();
43+
if (apiVersion is null)
44+
{
45+
return provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
46+
}
47+
48+
// V1 of the Delivery API uses a different expansion strategy than V2+
49+
return apiVersion.MajorVersion == 1
50+
? provider.GetRequiredService<RequestContextOutputExpansionStrategy>()
51+
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
52+
},
53+
ServiceLifetime.Scoped);
4554

46-
// V1 of the Delivery API uses a different expansion strategy than V2+
47-
return apiVersion.MajorVersion == 1
48-
? provider.GetRequiredService<RequestContextOutputExpansionStrategy>()
49-
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
50-
});
5155
builder.Services.AddSingleton<IRequestCultureService, RequestCultureService>();
5256
builder.Services.AddSingleton<IRequestRoutingService, RequestRoutingService>();
5357
builder.Services.AddSingleton<IRequestRedirectService, RequestRedirectService>();
5458
builder.Services.AddSingleton<IRequestPreviewService, RequestPreviewService>();
55-
builder.Services.AddSingleton<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>();
59+
60+
// Webooks register a more basic implementation, remove it.
61+
builder.Services.AddUnique<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>(ServiceLifetime.Singleton);
5662
builder.Services.AddSingleton<IRequestStartItemProviderAccessor, RequestContextRequestStartItemProviderAccessor>();
63+
5764
builder.Services.AddSingleton<IApiAccessService, ApiAccessService>();
5865
builder.Services.AddSingleton<IApiContentQueryService, ApiContentQueryService>();
5966
builder.Services.AddSingleton<IApiContentQueryProvider, ApiContentQueryProvider>();
Lines changed: 5 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,24 @@
11
using Microsoft.AspNetCore.Http;
22
using Microsoft.Extensions.Logging;
33
using Umbraco.Cms.Core.DeliveryApi;
4-
using Umbraco.Cms.Core.Models.PublishedContent;
5-
using Umbraco.Extensions;
4+
using Umbraco.Cms.Web.Common.Rendering;
65

76
namespace Umbraco.Cms.Api.Delivery.Rendering;
87

9-
internal sealed class RequestContextOutputExpansionStrategyV2 : IOutputExpansionStrategy
8+
internal sealed class RequestContextOutputExpansionStrategyV2 : ElementOnlyOutputExpansionStrategy, IOutputExpansionStrategy
109
{
11-
private const string All = "$all";
12-
private const string None = "";
13-
private const string ExpandParameterName = "expand";
14-
private const string FieldsParameterName = "fields";
15-
16-
private readonly IApiPropertyRenderer _propertyRenderer;
1710
private readonly ILogger<RequestContextOutputExpansionStrategyV2> _logger;
1811

19-
private readonly Stack<Node?> _expandProperties;
20-
private readonly Stack<Node?> _includeProperties;
21-
2212
public RequestContextOutputExpansionStrategyV2(
2313
IHttpContextAccessor httpContextAccessor,
2414
IApiPropertyRenderer propertyRenderer,
2515
ILogger<RequestContextOutputExpansionStrategyV2> logger)
16+
: base(propertyRenderer)
2617
{
27-
_propertyRenderer = propertyRenderer;
2818
_logger = logger;
29-
_expandProperties = new Stack<Node?>();
30-
_includeProperties = new Stack<Node?>();
31-
3219
InitializeExpandAndInclude(httpContextAccessor);
3320
}
3421

35-
public IDictionary<string, object?> MapContentProperties(IPublishedContent content)
36-
=> content.ItemType == PublishedItemType.Content
37-
? MapProperties(content.Properties)
38-
: throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}");
39-
40-
public IDictionary<string, object?> MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true)
41-
{
42-
if (media.ItemType != PublishedItemType.Media)
43-
{
44-
throw new ArgumentException($"Invalid item type. This method can only be used with item type {PublishedItemType.Media}, got: {media.ItemType}");
45-
}
46-
47-
IPublishedProperty[] properties = media
48-
.Properties
49-
.Where(p => skipUmbracoProperties is false || p.Alias.StartsWith("umbraco") is false)
50-
.ToArray();
51-
52-
return properties.Any()
53-
? MapProperties(properties)
54-
: new Dictionary<string, object?>();
55-
}
56-
57-
public IDictionary<string, object?> MapElementProperties(IPublishedElement element)
58-
=> MapProperties(element.Properties, true);
59-
6022
private void InitializeExpandAndInclude(IHttpContextAccessor httpContextAccessor)
6123
{
6224
string? QueryValue(string key) => httpContextAccessor.HttpContext?.Request.Query[key];
@@ -66,7 +28,7 @@ private void InitializeExpandAndInclude(IHttpContextAccessor httpContextAccessor
6628

6729
try
6830
{
69-
_expandProperties.Push(Node.Parse(toExpand));
31+
ExpandProperties.Push(Node.Parse(toExpand));
7032
}
7133
catch (ArgumentException ex)
7234
{
@@ -76,110 +38,12 @@ private void InitializeExpandAndInclude(IHttpContextAccessor httpContextAccessor
7638

7739
try
7840
{
79-
_includeProperties.Push(Node.Parse(toInclude));
41+
IncludeProperties.Push(Node.Parse(toInclude));
8042
}
8143
catch (ArgumentException ex)
8244
{
8345
_logger.LogError(ex, $"Could not parse the '{FieldsParameterName}' parameter. See exception for details.");
8446
throw new ArgumentException($"Could not parse the '{FieldsParameterName}' parameter: {ex.Message}");
8547
}
8648
}
87-
88-
private IDictionary<string, object?> MapProperties(IEnumerable<IPublishedProperty> properties, bool forceExpandProperties = false)
89-
{
90-
Node? currentExpandProperties = _expandProperties.Peek();
91-
if (_expandProperties.Count > 1 && currentExpandProperties is null && forceExpandProperties is false)
92-
{
93-
return new Dictionary<string, object?>();
94-
}
95-
96-
Node? currentIncludeProperties = _includeProperties.Peek();
97-
var result = new Dictionary<string, object?>();
98-
foreach (IPublishedProperty property in properties)
99-
{
100-
Node? nextIncludeProperties = GetNextProperties(currentIncludeProperties, property.Alias);
101-
if (currentIncludeProperties is not null && currentIncludeProperties.Items.Any() && nextIncludeProperties is null)
102-
{
103-
continue;
104-
}
105-
106-
Node? nextExpandProperties = GetNextProperties(currentExpandProperties, property.Alias);
107-
108-
_includeProperties.Push(nextIncludeProperties);
109-
_expandProperties.Push(nextExpandProperties);
110-
111-
result[property.Alias] = GetPropertyValue(property);
112-
113-
_expandProperties.Pop();
114-
_includeProperties.Pop();
115-
}
116-
117-
return result;
118-
}
119-
120-
private Node? GetNextProperties(Node? currentProperties, string propertyAlias)
121-
=> currentProperties?.Items.FirstOrDefault(i => i.Key == All)
122-
?? currentProperties?.Items.FirstOrDefault(i => i.Key == "properties")?.Items.FirstOrDefault(i => i.Key == All || i.Key == propertyAlias);
123-
124-
private object? GetPropertyValue(IPublishedProperty property)
125-
=> _propertyRenderer.GetPropertyValue(property, _expandProperties.Peek() is not null);
126-
127-
private class Node
128-
{
129-
public string Key { get; private set; } = string.Empty;
130-
131-
public List<Node> Items { get; } = new();
132-
133-
public static Node Parse(string value)
134-
{
135-
// verify that there are as many start brackets as there are end brackets
136-
if (value.CountOccurrences("[") != value.CountOccurrences("]"))
137-
{
138-
throw new ArgumentException("Value did not contain an equal number of start and end brackets");
139-
}
140-
141-
// verify that the value does not start with a start bracket
142-
if (value.StartsWith("["))
143-
{
144-
throw new ArgumentException("Value cannot start with a bracket");
145-
}
146-
147-
// verify that there are no empty brackets
148-
if (value.Contains("[]"))
149-
{
150-
throw new ArgumentException("Value cannot contain empty brackets");
151-
}
152-
153-
var stack = new Stack<Node>();
154-
var root = new Node { Key = "root" };
155-
stack.Push(root);
156-
157-
var currentNode = new Node();
158-
root.Items.Add(currentNode);
159-
160-
foreach (char c in value)
161-
{
162-
switch (c)
163-
{
164-
case '[': // Start a new node, child of the current node
165-
stack.Push(currentNode);
166-
currentNode = new Node();
167-
stack.Peek().Items.Add(currentNode);
168-
break;
169-
case ',': // Start a new node, but at the same level of the current node
170-
currentNode = new Node();
171-
stack.Peek().Items.Add(currentNode);
172-
break;
173-
case ']': // Back to parent of the current node
174-
currentNode = stack.Pop();
175-
break;
176-
default: // Add char to current node key
177-
currentNode.Key += c;
178-
break;
179-
}
180-
}
181-
182-
return root;
183-
}
184-
}
18549
}

src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilderExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.Extensions.DependencyInjection;
22
using Microsoft.Extensions.DependencyInjection.Extensions;
33
using Microsoft.Extensions.Logging;
4+
using Umbraco.Cms.Core.DeliveryApi;
45
using Umbraco.Cms.Core.DependencyInjection;
56
using Umbraco.Cms.Core.Hosting;
67
using Umbraco.Cms.Core.IO;
@@ -21,6 +22,8 @@
2122
using Umbraco.Cms.Web.BackOffice.Services;
2223
using Umbraco.Cms.Web.BackOffice.SignalR;
2324
using Umbraco.Cms.Web.BackOffice.Trees;
25+
using Umbraco.Cms.Web.Common.Accessors;
26+
using Umbraco.Cms.Web.Common.Rendering;
2427

2528
namespace Umbraco.Extensions;
2629

@@ -121,6 +124,9 @@ public static IUmbracoBuilder AddBackOfficeCore(this IUmbracoBuilder builder)
121124
builder.Services.AddSingleton<UnhandledExceptionLoggerMiddleware>();
122125
builder.Services.AddTransient<BlockGridSampleHelper>();
123126
builder.Services.AddUnique<IWebhookPresentationFactory, WebhookPresentationFactory>();
127+
// deliveryApi will overwrite these more basic ones.
128+
builder.Services.AddScoped<IOutputExpansionStrategy, ElementOnlyOutputExpansionStrategy>();
129+
builder.Services.AddSingleton<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>();
124130

125131
return builder;
126132
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Microsoft.AspNetCore.Http;
2+
using Umbraco.Cms.Core.DeliveryApi;
3+
4+
namespace Umbraco.Cms.Web.Common.Accessors;
5+
6+
public sealed class RequestContextOutputExpansionStrategyAccessor : RequestContextServiceAccessorBase<IOutputExpansionStrategy>, IOutputExpansionStrategyAccessor
7+
{
8+
public RequestContextOutputExpansionStrategyAccessor(IHttpContextAccessor httpContextAccessor)
9+
: base(httpContextAccessor)
10+
{
11+
}
12+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
using Microsoft.AspNetCore.Http;
3+
using Microsoft.Extensions.DependencyInjection;
4+
5+
namespace Umbraco.Cms.Web.Common.Accessors;
6+
7+
public abstract class RequestContextServiceAccessorBase<T>
8+
where T : class
9+
{
10+
private readonly IHttpContextAccessor _httpContextAccessor;
11+
12+
protected RequestContextServiceAccessorBase(IHttpContextAccessor httpContextAccessor)
13+
=> _httpContextAccessor = httpContextAccessor;
14+
15+
public bool TryGetValue([NotNullWhen(true)] out T? requestStartNodeService)
16+
{
17+
requestStartNodeService = _httpContextAccessor.HttpContext?.RequestServices.GetService<T>();
18+
return requestStartNodeService is not null;
19+
}
20+
}

0 commit comments

Comments
 (0)