diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html b/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html
index b55555339b..df649a9e4a 100644
--- a/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html
+++ b/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html
@@ -1,6 +1,6 @@
-
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js
index 64fc40d84d..3510da73a6 100644
--- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js
@@ -535,7 +535,7 @@
// remove all tabs except the specified tab
var tabs = scaffold.variants[0].tabs;
var tab = _.find(tabs, function (tab) {
- return tab.id !== 0 && (tab.alias.toLowerCase() === contentType.ncTabAlias.toLowerCase() || contentType.ncTabAlias === "");
+ return tab.id !== 0 && (tab.label.toLowerCase() === contentType.ncTabAlias.toLowerCase() || contentType.ncTabAlias === "");
});
scaffold.variants[0].tabs = [];
if (tab) {
diff --git a/src/Umbraco.Web.UI/Startup.cs b/src/Umbraco.Web.UI/Startup.cs
index 0d263c7b6b..71c3dd008c 100644
--- a/src/Umbraco.Web.UI/Startup.cs
+++ b/src/Umbraco.Web.UI/Startup.cs
@@ -15,10 +15,10 @@ namespace Umbraco.Cms.Web.UI
private readonly IConfiguration _config;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
- /// The Web Host Environment
- /// The Configuration
+ /// The web hosting environment.
+ /// The configuration.
///
/// Only a few services are possible to be injected here https://github.com/dotnet/aspnetcore/issues/9337
///
@@ -28,11 +28,10 @@ namespace Umbraco.Cms.Web.UI
_config = config ?? throw new ArgumentNullException(nameof(config));
}
-
-
///
- /// Configures the services
+ /// Configures the services.
///
+ /// The services.
///
/// This method gets called by the runtime. Use this method to add services to the container.
/// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
@@ -50,8 +49,10 @@ namespace Umbraco.Cms.Web.UI
}
///
- /// Configures the application
+ /// Configures the application.
///
+ /// The application builder.
+ /// The web hosting environment.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
@@ -59,6 +60,10 @@ namespace Umbraco.Cms.Web.UI
app.UseDeveloperExceptionPage();
}
+#if (UseHttpsRedirect)
+ app.UseHttpsRedirection();
+
+#endif
app.UseUmbraco()
.WithMiddleware(u =>
{
diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
index 7e2d51b12c..a050d22a16 100644
--- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
+++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj
@@ -66,12 +66,12 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs
index 62ec5a9921..797a4b2202 100644
--- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -1,16 +1,17 @@
using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Infrastructure.DependencyInjection;
using Umbraco.Cms.Web.Common.Middleware;
using Umbraco.Cms.Web.Common.Routing;
using Umbraco.Cms.Web.Website.Collections;
-using Umbraco.Cms.Web.Website.Controllers;
-using Umbraco.Cms.Web.Website.Middleware;
using Umbraco.Cms.Web.Website.Models;
using Umbraco.Cms.Web.Website.Routing;
using Umbraco.Cms.Web.Website.ViewEngines;
+using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor;
namespace Umbraco.Extensions
{
@@ -38,8 +39,9 @@ namespace Umbraco.Extensions
builder.Services.AddDataProtection();
builder.Services.AddAntiforgery();
- builder.Services.AddScoped();
+ builder.Services.AddSingleton();
builder.Services.AddSingleton();
+ builder.Services.TryAddEnumerable(Singleton());
builder.Services.AddSingleton();
builder.Services.AddSingleton();
@@ -47,7 +49,7 @@ namespace Umbraco.Extensions
builder.Services.AddSingleton();
- builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder
diff --git a/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs
index c549609397..33d42e07c9 100644
--- a/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs
+++ b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs
@@ -3,7 +3,6 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
using Umbraco.Cms.Web.Common.Middleware;
-using Umbraco.Cms.Web.Website.Middleware;
using Umbraco.Cms.Web.Website.Routing;
namespace Umbraco.Extensions
@@ -20,7 +19,6 @@ namespace Umbraco.Extensions
///
public static IUmbracoApplicationBuilderContext UseWebsite(this IUmbracoApplicationBuilderContext builder)
{
- builder.AppBuilder.UseMiddleware();
builder.AppBuilder.UseMiddleware();
return builder;
}
diff --git a/src/Umbraco.Web.Website/Routing/IPublicAccessRequestHandler.cs b/src/Umbraco.Web.Website/Routing/IPublicAccessRequestHandler.cs
new file mode 100644
index 0000000000..0ce3ddad92
--- /dev/null
+++ b/src/Umbraco.Web.Website/Routing/IPublicAccessRequestHandler.cs
@@ -0,0 +1,18 @@
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Umbraco.Cms.Web.Common.Routing;
+
+namespace Umbraco.Cms.Web.Website.Routing
+{
+ public interface IPublicAccessRequestHandler
+ {
+ ///
+ /// Ensures that access to current node is permitted.
+ ///
+ ///
+ /// The current route values
+ /// Updated route values if public access changes the rendered content, else the original route values.
+ /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture.
+ Task RewriteForPublishedContentAccessAsync(HttpContext httpContext, UmbracoRouteValues routeValues);
+ }
+}
diff --git a/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs b/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs
new file mode 100644
index 0000000000..dbc06175fd
--- /dev/null
+++ b/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Controllers;
+using Microsoft.AspNetCore.Routing;
+using Microsoft.AspNetCore.Routing.Matching;
+using Umbraco.Cms.Core.Routing;
+using Umbraco.Cms.Web.Common.Controllers;
+using Umbraco.Cms.Web.Common.Routing;
+
+namespace Umbraco.Cms.Web.Website.Routing
+{
+ ///
+ /// Used to handle 404 routes that haven't been handled by the end user
+ ///
+ internal class NotFoundSelectorPolicy : MatcherPolicy, IEndpointSelectorPolicy
+ {
+ private readonly Lazy _notFound;
+ private readonly EndpointDataSource _endpointDataSource;
+
+ public NotFoundSelectorPolicy(EndpointDataSource endpointDataSource)
+ {
+ _notFound = new Lazy(GetNotFoundEndpoint);
+ _endpointDataSource = endpointDataSource;
+ }
+
+ // return the endpoint for the RenderController.Index action.
+ private Endpoint GetNotFoundEndpoint()
+ {
+ Endpoint e = _endpointDataSource.Endpoints.First(x =>
+ {
+ // return the endpoint for the RenderController.Index action.
+ ControllerActionDescriptor descriptor = x.Metadata?.GetMetadata();
+ return descriptor?.ControllerTypeInfo == typeof(RenderController)
+ && descriptor?.ActionName == nameof(RenderController.Index);
+ });
+ return e;
+ }
+
+ public override int Order => 0;
+
+ public bool AppliesToEndpoints(IReadOnlyList endpoints)
+ {
+ // Don't apply this filter to any endpoint group that is a controller route
+ // i.e. only dynamic routes.
+ foreach (Endpoint endpoint in endpoints)
+ {
+ ControllerAttribute controller = endpoint.Metadata?.GetMetadata();
+ if (controller != null)
+ {
+ return false;
+ }
+ }
+
+ // then ensure this is only applied if all endpoints are IDynamicEndpointMetadata
+ return endpoints.All(x => x.Metadata?.GetMetadata() != null);
+ }
+
+ public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
+ {
+ if (AllInvalid(candidates))
+ {
+ UmbracoRouteValues umbracoRouteValues = httpContext.Features.Get();
+ if (umbracoRouteValues?.PublishedRequest != null
+ && !umbracoRouteValues.PublishedRequest.HasPublishedContent()
+ && umbracoRouteValues.PublishedRequest.ResponseStatusCode == StatusCodes.Status404NotFound)
+ {
+ // not found/404
+ httpContext.SetEndpoint(_notFound.Value);
+ }
+ }
+
+ return Task.CompletedTask;
+ }
+
+ private static bool AllInvalid(CandidateSet candidates)
+ {
+ for (int i = 0; i < candidates.Count; i++)
+ {
+ if (candidates.IsValidCandidate(i))
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/src/Umbraco.Web.Website/Middleware/PublicAccessMiddleware.cs b/src/Umbraco.Web.Website/Routing/PublicAccessRequestHandler.cs
similarity index 77%
rename from src/Umbraco.Web.Website/Middleware/PublicAccessMiddleware.cs
rename to src/Umbraco.Web.Website/Routing/PublicAccessRequestHandler.cs
index 8bbb4e13e4..88bb6622bd 100644
--- a/src/Umbraco.Web.Website/Middleware/PublicAccessMiddleware.cs
+++ b/src/Umbraco.Web.Website/Routing/PublicAccessRequestHandler.cs
@@ -10,22 +10,21 @@ using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Cms.Web.Common.Routing;
-using Umbraco.Cms.Web.Website.Routing;
using Umbraco.Extensions;
-namespace Umbraco.Cms.Web.Website.Middleware
+namespace Umbraco.Cms.Web.Website.Routing
{
- public class PublicAccessMiddleware : IMiddleware
+ public class PublicAccessRequestHandler : IPublicAccessRequestHandler
{
- private readonly ILogger _logger;
+ private readonly ILogger _logger;
private readonly IPublicAccessService _publicAccessService;
private readonly IPublicAccessChecker _publicAccessChecker;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IUmbracoRouteValuesFactory _umbracoRouteValuesFactory;
private readonly IPublishedRouter _publishedRouter;
- public PublicAccessMiddleware(
- ILogger logger,
+ public PublicAccessRequestHandler(
+ ILogger logger,
IPublicAccessService publicAccessService,
IPublicAccessChecker publicAccessChecker,
IUmbracoContextAccessor umbracoContextAccessor,
@@ -40,23 +39,8 @@ namespace Umbraco.Cms.Web.Website.Middleware
_publishedRouter = publishedRouter;
}
- public async Task InvokeAsync(HttpContext context, RequestDelegate next)
- {
- UmbracoRouteValues umbracoRouteValues = context.Features.Get();
-
- if (umbracoRouteValues != null)
- {
- await EnsurePublishedContentAccess(context, umbracoRouteValues);
- }
-
- await next(context);
- }
-
- ///
- /// Ensures that access to current node is permitted.
- ///
- /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture.
- private async Task EnsurePublishedContentAccess(HttpContext httpContext, UmbracoRouteValues routeValues)
+ ///
+ public async Task RewriteForPublishedContentAccessAsync(HttpContext httpContext, UmbracoRouteValues routeValues)
{
// because these might loop, we have to have some sort of infinite loop detection
int i = 0;
@@ -64,13 +48,13 @@ namespace Umbraco.Cms.Web.Website.Middleware
PublicAccessStatus publicAccessStatus = PublicAccessStatus.AccessAccepted;
do
{
- _logger.LogDebug(nameof(EnsurePublishedContentAccess) + ": Loop {LoopCounter}", i);
+ _logger.LogDebug(nameof(RewriteForPublishedContentAccessAsync) + ": Loop {LoopCounter}", i);
IPublishedContent publishedContent = routeValues.PublishedRequest?.PublishedContent;
if (publishedContent == null)
{
- return;
+ return routeValues;
}
var path = publishedContent.Path;
@@ -117,8 +101,10 @@ namespace Umbraco.Cms.Web.Website.Middleware
if (i == maxLoop)
{
- _logger.LogDebug(nameof(EnsurePublishedContentAccess) + ": Looks like we are running into an infinite loop, abort");
+ _logger.LogDebug(nameof(RewriteForPublishedContentAccessAsync) + ": Looks like we are running into an infinite loop, abort");
}
+
+ return routeValues;
}
@@ -139,17 +125,11 @@ namespace Umbraco.Cms.Web.Website.Middleware
// we need to change the content item that is getting rendered so we have to re-create UmbracoRouteValues.
UmbracoRouteValues updatedRouteValues = await _umbracoRouteValuesFactory.CreateAsync(httpContext, reRouted);
- // Update the feature
- httpContext.Features.Set(updatedRouteValues);
-
return updatedRouteValues;
}
else
{
- _logger.LogWarning("Public Access rule has a redirect node set to itself, nothing can be routed.");
- // Update the feature to nothing - cannot continue
- httpContext.Features.Set(null);
- return null;
+ throw new InvalidOperationException("Public Access rule has a redirect node set to itself, nothing can be routed.");
}
}
}
diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs
index 41fd20a69b..0bca8d7215 100644
--- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs
+++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs
@@ -28,6 +28,7 @@ using RouteDirection = Umbraco.Cms.Core.Routing.RouteDirection;
namespace Umbraco.Cms.Web.Website.Routing
{
+
///
/// The route value transformer for Umbraco front-end routes
///
@@ -51,6 +52,7 @@ namespace Umbraco.Cms.Web.Website.Routing
private readonly IDataProtectionProvider _dataProtectionProvider;
private readonly IControllerActionSearcher _controllerActionSearcher;
private readonly IEventAggregator _eventAggregator;
+ private readonly IPublicAccessRequestHandler _publicAccessRequestHandler;
///
/// Initializes a new instance of the class.
@@ -66,7 +68,8 @@ namespace Umbraco.Cms.Web.Website.Routing
IRoutableDocumentFilter routableDocumentFilter,
IDataProtectionProvider dataProtectionProvider,
IControllerActionSearcher controllerActionSearcher,
- IEventAggregator eventAggregator)
+ IEventAggregator eventAggregator,
+ IPublicAccessRequestHandler publicAccessRequestHandler)
{
if (globalSettings is null)
{
@@ -84,6 +87,7 @@ namespace Umbraco.Cms.Web.Website.Routing
_dataProtectionProvider = dataProtectionProvider;
_controllerActionSearcher = controllerActionSearcher;
_eventAggregator = eventAggregator;
+ _publicAccessRequestHandler = publicAccessRequestHandler;
}
///
@@ -92,31 +96,54 @@ namespace Umbraco.Cms.Web.Website.Routing
// If we aren't running, then we have nothing to route
if (_runtime.Level != RuntimeLevel.Run)
{
- return values;
+ return null;
}
// will be null for any client side requests like JS, etc...
- if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext))
+ if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext umbracoContext))
{
- return values;
+ return null;
}
if (!_routableDocumentFilter.IsDocumentRequest(httpContext.Request.Path))
{
- return values;
+ return null;
+ }
+
+ // Don't execute if there are already UmbracoRouteValues assigned.
+ // This can occur if someone else is dynamically routing and in which case we don't want to overwrite
+ // the routing work being done there.
+ UmbracoRouteValues umbracoRouteValues = httpContext.Features.Get();
+ if (umbracoRouteValues != null)
+ {
+ return null;
}
// Check if there is no existing content and return the no content controller
if (!umbracoContext.Content.HasContent())
{
- values[ControllerToken] = ControllerExtensions.GetControllerName();
- values[ActionToken] = nameof(RenderNoContentController.Index);
-
- return values;
+ return new RouteValueDictionary
+ {
+ [ControllerToken] = ControllerExtensions.GetControllerName(),
+ [ActionToken] = nameof(RenderNoContentController.Index)
+ };
}
- IPublishedRequest publishedRequest = await RouteRequestAsync(httpContext, umbracoContext);
+ IPublishedRequest publishedRequest = await RouteRequestAsync(umbracoContext);
- UmbracoRouteValues umbracoRouteValues = await _routeValuesFactory.CreateAsync(httpContext, publishedRequest);
+ umbracoRouteValues = await _routeValuesFactory.CreateAsync(httpContext, publishedRequest);
+
+ if (!umbracoRouteValues?.PublishedRequest?.HasPublishedContent() ?? false)
+ {
+ // No content was found, not by any registered 404 handlers and
+ // not by the IContentLastChanceFinder. In this case we want to return
+ // our default 404 page but we cannot return route values now because
+ // it's possible that a developer is handling dynamic routes too.
+ // Our 404 page will be handled with the NotFoundSelectorPolicy
+ return null;
+ }
+
+ // now we need to do some public access checks
+ umbracoRouteValues = await _publicAccessRequestHandler.RewriteForPublishedContentAccessAsync(httpContext, umbracoRouteValues);
// Store the route values as a httpcontext feature
httpContext.Features.Set(umbracoRouteValues);
@@ -125,19 +152,25 @@ namespace Umbraco.Cms.Web.Website.Routing
PostedDataProxyInfo postedInfo = GetFormInfo(httpContext, values);
if (postedInfo != null)
{
- return HandlePostedValues(postedInfo, httpContext, values);
+ return HandlePostedValues(postedInfo, httpContext);
}
- values[ControllerToken] = umbracoRouteValues.ControllerName;
+ // See https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.dynamicroutevaluetransformer.transformasync?view=aspnetcore-5.0#Microsoft_AspNetCore_Mvc_Routing_DynamicRouteValueTransformer_TransformAsync_Microsoft_AspNetCore_Http_HttpContext_Microsoft_AspNetCore_Routing_RouteValueDictionary_
+ // We should apparenlty not be modified these values.
+ // So we create new ones.
+ var newValues = new RouteValueDictionary
+ {
+ [ControllerToken] = umbracoRouteValues.ControllerName
+ };
if (string.IsNullOrWhiteSpace(umbracoRouteValues.ActionName) == false)
{
- values[ActionToken] = umbracoRouteValues.ActionName;
+ newValues[ActionToken] = umbracoRouteValues.ActionName;
}
- return values;
+ return newValues;
}
- private async Task RouteRequestAsync(HttpContext httpContext, IUmbracoContext umbracoContext)
+ private async Task RouteRequestAsync(IUmbracoContext umbracoContext)
{
// ok, process
@@ -196,11 +229,14 @@ namespace Umbraco.Cms.Web.Website.Routing
};
}
- private RouteValueDictionary HandlePostedValues(PostedDataProxyInfo postedInfo, HttpContext httpContext, RouteValueDictionary values)
+ private RouteValueDictionary HandlePostedValues(PostedDataProxyInfo postedInfo, HttpContext httpContext)
{
// set the standard route values/tokens
- values[ControllerToken] = postedInfo.ControllerName;
- values[ActionToken] = postedInfo.ActionName;
+ var values = new RouteValueDictionary
+ {
+ [ControllerToken] = postedInfo.ControllerName,
+ [ActionToken] = postedInfo.ActionName
+ };
ControllerActionDescriptor surfaceControllerDescriptor = _controllerActionSearcher.Find(httpContext, postedInfo.ControllerName, postedInfo.ActionName);
diff --git a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj
index d1bd53e5fe..7dab73b100 100644
--- a/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj
+++ b/src/Umbraco.Web.Website/Umbraco.Web.Website.csproj
@@ -32,7 +32,7 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all