Skip to content

Commit ab15543

Browse files
committed
Fixes #446, #439, #432
1 parent 1e8579d commit ab15543

15 files changed

+732
-1030
lines changed

src/Articulate.Tests.Website/appsettings-schema.json

+610-1,011
Large diffs are not rendered by default.

src/Articulate/Articulate.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
3+
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
44
<OutputType>Library</OutputType>
55
<SolutionDir Condition="$(SolutionDir) == '' Or $(SolutionDir) == '*Undefined*'">..\</SolutionDir>
66
<RestorePackages>true</RestorePackages>

src/Articulate/Components/ArticulateComposer.cs

+3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
using Articulate.Routing;
66
using Articulate.Services;
77
using Articulate.Syndication;
8+
using Microsoft.AspNetCore.Routing;
89
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.DependencyInjection.Extensions;
911
using Umbraco.Cms.Core.Composing;
1012
using Umbraco.Cms.Core.DependencyInjection;
1113
using Umbraco.Cms.Core.Notifications;
@@ -36,6 +38,7 @@ public override void Compose(IUmbracoBuilder builder)
3638
services.AddSingleton<ArticulateRouter>();
3739
services.AddSingleton<RouteCacheRefresherFilter>();
3840
services.AddSingleton<ArticulateFrontEndFilterConvention>();
41+
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<MatcherPolicy, ArticulateDynamicRouteSelectorPolicy>());
3942

4043
builder.UrlProviders().InsertBefore<DefaultUrlProvider, DateFormattedUrlProvider>();
4144

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace Articulate.Controllers
8+
{
9+
[AttributeUsage(AttributeTargets.Class)]
10+
public sealed class ArticulateDynamicRouteAttribute : Attribute
11+
{
12+
}
13+
}

src/Articulate/Controllers/ArticulateRssController.cs

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ namespace Articulate.Controllers
2727
#if NET7_0_OR_GREATER
2828
[OutputCache(PolicyName = "Articulate300")]
2929
#endif
30+
[ArticulateDynamicRoute]
3031
public class ArticulateRssController : RenderController
3132
{
3233
private readonly IRssFeedGenerator _feedGenerator;

src/Articulate/Controllers/ArticulateSearchController.cs

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ namespace Articulate.Controllers
1717
/// <summary>
1818
/// Renders search results
1919
/// </summary>
20+
[ArticulateDynamicRoute]
2021
public class ArticulateSearchController : ListControllerBase
2122
{
2223
private readonly IArticulateSearcher _articulateSearcher;

src/Articulate/Controllers/ArticulateTagsController.cs

+5-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
using Articulate.Services;
1414
using Umbraco.Cms.Core.PublishedCache;
1515
using System.Collections.Generic;
16-
#if NET7_0_OR_GREATER
16+
using Umbraco.Cms.Web.Common.Attributes;
17+
18+
#if NET7_0_OR_GREATER
1719
using Microsoft.AspNetCore.OutputCaching;
1820
#endif
1921

@@ -28,7 +30,7 @@ namespace Articulate.Controllers
2830
#if NET7_0_OR_GREATER
2931
[OutputCache(PolicyName = "Articulate60")]
3032
#endif
31-
33+
[ArticulateDynamicRoute]
3234
public class ArticulateTagsController : ListControllerBase
3335
{
3436
private readonly UmbracoHelper _umbracoHelper;
@@ -51,7 +53,7 @@ public ArticulateTagsController(
5153
_articulateTagService = articulateTagService;
5254
_tagQuery = tagQuery;
5355
}
54-
56+
5557
/// <summary>
5658
/// Used to render the category listing (virtual node)
5759
/// </summary>

src/Articulate/Controllers/MarkdownEditorController.cs

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
namespace Articulate.Controllers
1414
{
15+
[ArticulateDynamicRoute]
1516
public class MarkdownEditorController : RenderController
1617
{
1718
private readonly UmbracoApiControllerTypeCollection _apiControllers;

src/Articulate/Controllers/MetaWeblogController.cs

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ namespace Articulate.Controllers
2323
/// middleware but that just supports one endpoint, we are basically wrapping that
2424
/// with our own multi-tenanted version.
2525
/// </remarks>
26+
[ArticulateDynamicRoute]
2627
public class MetaWeblogController : RenderController
2728
{
2829
private readonly IServiceProvider _serviceProvider;

src/Articulate/Controllers/OpenSearchController.cs

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
namespace Articulate.Controllers
1212
{
13+
[ArticulateDynamicRoute]
1314
public class OpenSearchController : RenderController
1415
{
1516
private readonly IPublishedValueFallback _publishedValueFallback;

src/Articulate/Controllers/RsdController.cs

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ namespace Articulate.Controllers
1515
/// <summary>
1616
/// Really simple discovery controller
1717
/// </summary>
18+
[ArticulateDynamicRoute]
1819
public class RsdController : RenderController
1920
{
2021
private readonly UmbracoHelper _umbracoHelper;

src/Articulate/Controllers/WlwManifestController.cs

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
namespace Articulate.Controllers
1010
{
11+
[ArticulateDynamicRoute]
1112
public class WlwManifestController : RenderController
1213
{
1314
private readonly UmbracoHelper _umbraco;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#nullable enable
2+
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Threading.Tasks;
6+
using Articulate.Controllers;
7+
using Microsoft.AspNetCore.Http;
8+
using Microsoft.AspNetCore.Mvc;
9+
using Microsoft.AspNetCore.Routing;
10+
using Microsoft.AspNetCore.Routing.Matching;
11+
using Umbraco.Cms.Web.Common.Routing;
12+
13+
namespace Articulate.Routing
14+
{
15+
/// <summary>
16+
/// Used when their is ambiguous route candidates due to multiple dynamic routes being assigned.
17+
/// </summary>
18+
/// <remarks>
19+
/// Ambiguous dynamic routes can occur if Umbraco detects a 404 and assigns a route, but sometimes its not
20+
/// actually a 404 because the articulate router occurs after the Umbraco router which handles 404 eagerly.
21+
/// This causes 2x candidates to be resolved and the first (umbraco) is chosen.
22+
/// If we detect that Articulate actually performed the routing, then we use that candidate instead.
23+
/// TODO: Ideally - Umbraco would dynamically route the 404 in a much later state which could be done,
24+
/// by a dynamic router that has a much larger Order so it occurs later in the pipeline instead of eagerly.
25+
/// </remarks>
26+
internal class ArticulateDynamicRouteSelectorPolicy : MatcherPolicy, IEndpointSelectorPolicy
27+
{
28+
public override int Order => 100;
29+
30+
public bool AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)
31+
{
32+
// Don't apply this filter to any endpoint group that is a controller route
33+
// i.e. only dynamic routes.
34+
foreach (Endpoint endpoint in endpoints)
35+
{
36+
ControllerAttribute? controller = endpoint.Metadata.GetMetadata<ControllerAttribute>();
37+
if (controller != null)
38+
{
39+
return false;
40+
}
41+
}
42+
43+
// then ensure this is only applied if all endpoints are IDynamicEndpointMetadata
44+
return endpoints.All(x => x.Metadata.GetMetadata<IDynamicEndpointMetadata>() != null);
45+
}
46+
47+
public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
48+
{
49+
var umbracoRouteValues = httpContext.Features.Get<UmbracoRouteValues>();
50+
51+
// If the request has been dynamically routed by articulate to an
52+
// Articulate controller
53+
if (umbracoRouteValues != null
54+
&& umbracoRouteValues.ControllerActionDescriptor.EndpointMetadata.Any(x => x is ArticulateDynamicRouteAttribute))
55+
{
56+
for (var i = 0; i < candidates.Count; i++)
57+
{
58+
// If the candidate is an Articulate dynamic controller, set valid
59+
if (candidates[i].Endpoint.Metadata.GetMetadata<ArticulateDynamicRouteAttribute>() is not null)
60+
{
61+
candidates.SetValidity(i, true);
62+
}
63+
else
64+
{
65+
// else it is invalid
66+
candidates.SetValidity(i, false);
67+
}
68+
}
69+
}
70+
71+
return Task.CompletedTask;
72+
}
73+
}
74+
}

src/Articulate/Routing/ArticulateRouteValueTransformer.cs

+9-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#nullable enable
2+
13
using Microsoft.AspNetCore.Http;
24
using Microsoft.AspNetCore.Mvc.Routing;
35
using Microsoft.AspNetCore.Routing;
@@ -148,6 +150,8 @@ private async Task WriteRouteValues(IUmbracoContext umbracoContext, HttpContext
148150
// Store the route values as a httpcontext feature
149151
httpContext.Features.Set(umbracoRouteValues);
150152

153+
umbracoContext.PublishedRequest = publishedRequest;
154+
151155
values[ControllerToken] = dynamicRouteValues.ControllerActionDescriptor.ControllerName;
152156
if (string.IsNullOrWhiteSpace(dynamicRouteValues.ControllerActionDescriptor.ActionName) == false)
153157
{
@@ -175,9 +179,11 @@ private bool ShouldCheck(
175179
return false;
176180
}
177181

178-
// If route values have already been assigned, then Umbraco has
179-
// matched content, we will not proceed.
180-
if (umbracoRouteValues?.PublishedRequest?.PublishedContent != null)
182+
// If route values have already been assigned, then Umbraco has matched content, we will not proceed.
183+
// A 404 can be matched by Umbraco too which will occur for Articulate dynamic routes, so we need to
184+
// proceed to see if it is actually a 404.
185+
if (umbracoRouteValues?.PublishedRequest?.PublishedContent != null
186+
&& umbracoRouteValues?.PublishedRequest?.ResponseStatusCode != 404)
181187
{
182188
return false;
183189
}

src/Articulate/Routing/ArticulateRouter.cs

+10-12
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,14 @@ public class ArticulateRouter
2020
{
2121
private static readonly object s_locker = new object();
2222
private static readonly string s_searchControllerName = ControllerExtensions.GetControllerName<ArticulateSearchController>();
23-
private static readonly string s_openSearchControllerName = ControllerExtensions.GetControllerName<OpenSearchController>();
24-
private static readonly string s_rsdControllerName = ControllerExtensions.GetControllerName<RsdController>();
25-
private static readonly string s_wlwControllerName = ControllerExtensions.GetControllerName<WlwManifestController>();
23+
private static readonly string s_openSearchControllerName = ControllerExtensions.GetControllerName<OpenSearchController>();
24+
private static readonly string s_rsdControllerName = ControllerExtensions.GetControllerName<RsdController>();
25+
private static readonly string s_wlwControllerName = ControllerExtensions.GetControllerName<WlwManifestController>();
2626
private static readonly string s_tagsControllerName = ControllerExtensions.GetControllerName<ArticulateTagsController>();
2727
private static readonly string s_rssControllerName = ControllerExtensions.GetControllerName<ArticulateRssController>();
2828
private static readonly string s_markdownEditorControllerName = ControllerExtensions.GetControllerName<MarkdownEditorController>();
2929
private static readonly string s_metaWeblogControllerName = ControllerExtensions.GetControllerName<MetaWeblogController>();
3030

31-
32-
3331
private readonly Dictionary<ArticulateRouteTemplate, ArticulateRootNodeCache> _routeCache = new();
3432
private readonly IControllerActionSearcher _controllerActionSearcher;
3533

@@ -44,7 +42,7 @@ public ArticulateRouter(IControllerActionSearcher controllerActionSearcher)
4442

4543
public bool TryMatch(PathString path, RouteValueDictionary routeValues, out ArticulateRootNodeCache articulateRootNodeCache)
4644
{
47-
foreach(var item in _routeCache)
45+
foreach (var item in _routeCache)
4846
{
4947
var templateMatcher = new TemplateMatcher(item.Key.RouteTemplate, routeValues);
5048
if (templateMatcher.TryMatch(path, routeValues))
@@ -116,17 +114,17 @@ public void MapRoutes(HttpContext httpContext, IUmbracoContext umbracoContext)
116114
MapAuthorsRssRoute(httpContext, rootNodePath, articulateRootNode, domains);
117115

118116
MapSearchRoute(httpContext, rootNodePath, articulateRootNode, domains);
119-
MapMetaWeblogRoute(httpContext, rootNodePath, articulateRootNode, domains);
120-
MapManifestRoute(httpContext, rootNodePath, articulateRootNode, domains);
117+
MapMetaWeblogRoute(httpContext, rootNodePath, articulateRootNode, domains);
118+
MapManifestRoute(httpContext, rootNodePath, articulateRootNode, domains);
121119
MapRsdRoute(httpContext, rootNodePath, articulateRootNode, domains);
122120
MapOpenSearchRoute(httpContext, rootNodePath, articulateRootNode, domains);
123121

124122
// tags/cats routes are the least specific
125123
MapTagsAndCategoriesRoute(httpContext, rootNodePath, articulateRootNode, domains);
126124
}
127-
}
125+
}
128126
}
129-
}
127+
}
130128

131129
/// <summary>
132130
/// Generically caches a url path for a particular controller
@@ -153,7 +151,7 @@ private void MapRoute(
153151
_routeCache[art] = dynamicRouteValues;
154152
}
155153

156-
dynamicRouteValues.Add(articulateRootNode.Id, DomainsForContent(articulateRootNode,domains));
154+
dynamicRouteValues.Add(articulateRootNode.Id, DomainsForContent(articulateRootNode, domains));
157155
}
158156

159157
private List<Domain> DomainsForContent(IPublishedContent content, IReadOnlyList<Domain> domains)
@@ -175,7 +173,7 @@ private void MapOpenSearchRoute(HttpContext httpContext, string rootNodePath, IP
175173
domains);
176174
}
177175

178-
private void MapRsdRoute(HttpContext httpContext, string rootNodePath, IPublishedContent articulateRootNode, List<Domain> domains)
176+
private void MapRsdRoute(HttpContext httpContext, string rootNodePath, IPublishedContent articulateRootNode, List<Domain> domains)
179177
{
180178
RouteTemplate template = TemplateParser.Parse($"{rootNodePath}rsd/{{id}}");
181179
MapRoute(

0 commit comments

Comments
 (0)