Skip to content

Commit 62c1d44

Browse files
authored
Webhooks: Register OutputExpansionStrategy for webhooks if Delivery API is not enabled (#20559)
* Register slimmed down OutputExpansionStrategy for webhooks if deliveryapi is not enabled * PR review comment resolution
1 parent c2eea5d commit 62c1d44

File tree

6 files changed

+214
-154
lines changed

6 files changed

+214
-154
lines changed
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.Api.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.Api.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+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
using Umbraco.Cms.Core.DeliveryApi;
2+
using Umbraco.Cms.Core.Models.PublishedContent;
3+
using Umbraco.Extensions;
4+
5+
namespace Umbraco.Cms.Api.Common.Rendering;
6+
7+
public class ElementOnlyOutputExpansionStrategy : IOutputExpansionStrategy
8+
{
9+
protected const string All = "$all";
10+
protected const string None = "";
11+
protected const string ExpandParameterName = "expand";
12+
protected const string FieldsParameterName = "fields";
13+
14+
private readonly IApiPropertyRenderer _propertyRenderer;
15+
16+
protected Stack<Node?> ExpandProperties { get; } = new();
17+
18+
protected Stack<Node?> IncludeProperties { get; } = new();
19+
20+
public ElementOnlyOutputExpansionStrategy(
21+
IApiPropertyRenderer propertyRenderer)
22+
{
23+
_propertyRenderer = propertyRenderer;
24+
}
25+
26+
public virtual IDictionary<string, object?> MapContentProperties(IPublishedContent content)
27+
=> content.ItemType == PublishedItemType.Content
28+
? MapProperties(content.Properties)
29+
: throw new ArgumentException($"Invalid item type. This method can only be used with item type {nameof(PublishedItemType.Content)}, got: {content.ItemType}");
30+
31+
public virtual IDictionary<string, object?> MapMediaProperties(IPublishedContent media, bool skipUmbracoProperties = true)
32+
{
33+
if (media.ItemType != PublishedItemType.Media)
34+
{
35+
throw new ArgumentException($"Invalid item type. This method can only be used with item type {PublishedItemType.Media}, got: {media.ItemType}");
36+
}
37+
38+
IPublishedProperty[] properties = media
39+
.Properties
40+
.Where(p => skipUmbracoProperties is false || p.Alias.StartsWith("umbraco") is false)
41+
.ToArray();
42+
43+
return properties.Any()
44+
? MapProperties(properties)
45+
: new Dictionary<string, object?>();
46+
}
47+
48+
public virtual IDictionary<string, object?> MapElementProperties(IPublishedElement element)
49+
=> MapProperties(element.Properties, true);
50+
51+
private IDictionary<string, object?> MapProperties(IEnumerable<IPublishedProperty> properties, bool forceExpandProperties = false)
52+
{
53+
Node? currentExpandProperties = ExpandProperties.Count > 0 ? ExpandProperties.Peek() : null;
54+
if (ExpandProperties.Count > 1 && currentExpandProperties is null && forceExpandProperties is false)
55+
{
56+
return new Dictionary<string, object?>();
57+
}
58+
59+
Node? currentIncludeProperties = IncludeProperties.Count > 0 ? IncludeProperties.Peek() : null;
60+
var result = new Dictionary<string, object?>();
61+
foreach (IPublishedProperty property in properties)
62+
{
63+
Node? nextIncludeProperties = GetNextProperties(currentIncludeProperties, property.Alias);
64+
if (currentIncludeProperties is not null && currentIncludeProperties.Items.Any() && nextIncludeProperties is null)
65+
{
66+
continue;
67+
}
68+
69+
Node? nextExpandProperties = GetNextProperties(currentExpandProperties, property.Alias);
70+
71+
IncludeProperties.Push(nextIncludeProperties);
72+
ExpandProperties.Push(nextExpandProperties);
73+
74+
result[property.Alias] = GetPropertyValue(property);
75+
76+
ExpandProperties.Pop();
77+
IncludeProperties.Pop();
78+
}
79+
80+
return result;
81+
}
82+
83+
private Node? GetNextProperties(Node? currentProperties, string propertyAlias)
84+
=> currentProperties?.Items.FirstOrDefault(i => i.Key == All)
85+
?? currentProperties?.Items.FirstOrDefault(i => i.Key == "properties")?.Items.FirstOrDefault(i => i.Key == All || i.Key == propertyAlias);
86+
87+
private object? GetPropertyValue(IPublishedProperty property)
88+
=> _propertyRenderer.GetPropertyValue(property, ExpandProperties.Peek() is not null);
89+
90+
protected sealed class Node
91+
{
92+
public string Key { get; private set; } = string.Empty;
93+
94+
public List<Node> Items { get; } = new();
95+
96+
public static Node Parse(string value)
97+
{
98+
// verify that there are as many start brackets as there are end brackets
99+
if (value.CountOccurrences("[") != value.CountOccurrences("]"))
100+
{
101+
throw new ArgumentException("Value did not contain an equal number of start and end brackets");
102+
}
103+
104+
// verify that the value does not start with a start bracket
105+
if (value.StartsWith("["))
106+
{
107+
throw new ArgumentException("Value cannot start with a bracket");
108+
}
109+
110+
// verify that there are no empty brackets
111+
if (value.Contains("[]"))
112+
{
113+
throw new ArgumentException("Value cannot contain empty brackets");
114+
}
115+
116+
var stack = new Stack<Node>();
117+
var root = new Node { Key = "root" };
118+
stack.Push(root);
119+
120+
var currentNode = new Node();
121+
root.Items.Add(currentNode);
122+
123+
foreach (char c in value)
124+
{
125+
switch (c)
126+
{
127+
case '[': // Start a new node, child of the current node
128+
stack.Push(currentNode);
129+
currentNode = new Node();
130+
stack.Peek().Items.Add(currentNode);
131+
break;
132+
case ',': // Start a new node, but at the same level of the current node
133+
currentNode = new Node();
134+
stack.Peek().Items.Add(currentNode);
135+
break;
136+
case ']': // Back to parent of the current node
137+
currentNode = stack.Pop();
138+
break;
139+
default: // Add char to current node key
140+
currentNode.Key += c;
141+
break;
142+
}
143+
}
144+
145+
return root;
146+
}
147+
}
148+
}

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

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

47-
// V1 of the Delivery API uses a different expansion strategy than V2+
48-
return apiVersion.MajorVersion == 1
49-
? provider.GetRequiredService<RequestContextOutputExpansionStrategy>()
50-
: provider.GetRequiredService<RequestContextOutputExpansionStrategyV2>();
51-
});
5256
builder.Services.AddSingleton<IRequestCultureService, RequestCultureService>();
5357
builder.Services.AddSingleton<IRequestSegmmentService, RequestSegmentService>();
5458
builder.Services.AddSingleton<IRequestSegmentService, RequestSegmentService>();
5559
builder.Services.AddSingleton<IRequestRoutingService, RequestRoutingService>();
5660
builder.Services.AddSingleton<IRequestRedirectService, RequestRedirectService>();
5761
builder.Services.AddSingleton<IRequestPreviewService, RequestPreviewService>();
58-
builder.Services.AddSingleton<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>();
62+
63+
// Webooks register a more basic implementation, remove it.
64+
builder.Services.AddUnique<IOutputExpansionStrategyAccessor, RequestContextOutputExpansionStrategyAccessor>(ServiceLifetime.Singleton);
5965
builder.Services.AddSingleton<IRequestStartItemProviderAccessor, RequestContextRequestStartItemProviderAccessor>();
66+
6067
builder.Services.AddSingleton<IApiAccessService, ApiAccessService>();
6168
builder.Services.AddSingleton<IApiContentQueryService, ApiContentQueryService>();
6269
builder.Services.AddSingleton<IApiContentQueryProvider, ApiContentQueryProvider>();

0 commit comments

Comments
 (0)