From e3be4009c02e941f2a89411598c91a8cc3321a4e Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 8 Dec 2020 16:33:50 +1100 Subject: [PATCH] Getting front end routing poc going --- src/Umbraco.Core/Routing/PublishedRouter.cs | 7 +- .../{ => Web}/HybridUmbracoContextAccessor.cs | 0 src/Umbraco.Core/{ => Web}/IUmbracoContext.cs | 0 .../{ => Web}/IUmbracoContextAccessor.cs | 0 .../{ => Web}/IUmbracoContextFactory.cs | 0 ...RenderIndexActionSelectorAttributeTests.cs | 17 +- .../Controllers/SurfaceControllerTests.cs | 1 + .../Controllers/BackOfficeController.cs | 2 +- .../BackOfficeApplicationBuilderExtensions.cs | 49 ++-- .../PreviewAuthenticationMiddleware.cs | 6 +- .../Trees/ApplicationTreeController.cs | 34 +-- .../Controllers/IRenderController.cs | 6 +- .../Controllers/IRenderMvcController.cs | 19 -- .../Controllers/RenderController.cs | 10 - .../Controllers/UmbracoController.cs | 11 +- .../ApplicationBuilderExtensions.cs | 110 ++++++--- ...racoInstallApplicationBuilderExtensions.cs | 17 +- ...tialViewMacroViewContextFilterAttribute.cs | 35 +-- .../PreRenderViewActionFilterAttribute.cs | 43 ---- .../Middleware/UmbracoRequestMiddleware.cs | 22 +- src/Umbraco.Web.Common/Routing/IAreaRoutes.cs | 6 +- src/Umbraco.Web.UI.NetCore/Startup.cs | 1 + .../Controllers/IUmbracoRenderingDefaults.cs | 15 ++ ...erMvcController.cs => RenderController.cs} | 34 ++- .../RenderIndexActionSelectorAttribute.cs | 23 +- .../Controllers/SurfaceController.cs | 3 +- .../Controllers/UmbracoRenderingDefaults.cs | 13 + .../Extensions/UmbracoBuilderExtensions.cs | 17 +- ...racoWebsiteApplicationBuilderExtensions.cs | 31 ++- .../Routing/NoContentRoutes.cs | 19 +- .../{ => Routing}/RouteDefinition.cs | 4 +- .../Routing/UmbracoRouteValueTransformer.cs | 230 ++++++++++++++++++ .../Mvc/RenderControllerFactory.cs | 45 ---- src/Umbraco.Web/Mvc/RenderRouteHandler.cs | 10 +- src/Umbraco.Web/Mvc/RouteDefinition.cs | 8 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 - src/Umbraco.Web/UmbracoInjectedModule.cs | 9 +- 37 files changed, 543 insertions(+), 315 deletions(-) rename src/Umbraco.Core/{ => Web}/HybridUmbracoContextAccessor.cs (100%) rename src/Umbraco.Core/{ => Web}/IUmbracoContext.cs (100%) rename src/Umbraco.Core/{ => Web}/IUmbracoContextAccessor.cs (100%) rename src/Umbraco.Core/{ => Web}/IUmbracoContextFactory.cs (100%) delete mode 100644 src/Umbraco.Web.Common/Controllers/IRenderMvcController.cs delete mode 100644 src/Umbraco.Web.Common/Controllers/RenderController.cs delete mode 100644 src/Umbraco.Web.Common/Filters/PreRenderViewActionFilterAttribute.cs create mode 100644 src/Umbraco.Web.Website/Controllers/IUmbracoRenderingDefaults.cs rename src/Umbraco.Web.Website/Controllers/{RenderMvcController.cs => RenderController.cs} (78%) create mode 100644 src/Umbraco.Web.Website/Controllers/UmbracoRenderingDefaults.cs rename src/Umbraco.Web.Website/{ => Routing}/RouteDefinition.cs (94%) create mode 100644 src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs delete mode 100644 src/Umbraco.Web/Mvc/RenderControllerFactory.cs diff --git a/src/Umbraco.Core/Routing/PublishedRouter.cs b/src/Umbraco.Core/Routing/PublishedRouter.cs index 9b7354de74..10986b941a 100644 --- a/src/Umbraco.Core/Routing/PublishedRouter.cs +++ b/src/Umbraco.Core/Routing/PublishedRouter.cs @@ -138,7 +138,7 @@ namespace Umbraco.Web.Routing Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture = request.Culture; SetVariationContext(request.Culture.Name); - //find the published content if it's not assigned. This could be manually assigned with a custom route handler, or + // find the published content if it's not assigned. This could be manually assigned with a custom route handler, or // with something like EnsurePublishedContentRequestAttribute or UmbracoVirtualNodeRouteHandler. Those in turn call this method // to setup the rest of the pipeline but we don't want to run the finders since there's one assigned. if (request.PublishedContent == null) @@ -156,15 +156,13 @@ namespace Umbraco.Web.Routing // trigger the Prepared event - at that point it is still possible to change about anything // even though the request might be flagged for redirection - we'll redirect _after_ the event - // // also, OnPrepared() will make the PublishedRequest readonly, so nothing can change - // request.OnPrepared(); // we don't take care of anything so if the content has changed, it's up to the user // to find out the appropriate template - //complete the PCR and assign the remaining values + // complete the PCR and assign the remaining values return ConfigureRequest(request); } @@ -201,7 +199,6 @@ namespace Umbraco.Web.Routing // can't go beyond that point without a PublishedContent to render // it's ok not to have a template, in order to give MVC a chance to hijack routes - return true; } diff --git a/src/Umbraco.Core/HybridUmbracoContextAccessor.cs b/src/Umbraco.Core/Web/HybridUmbracoContextAccessor.cs similarity index 100% rename from src/Umbraco.Core/HybridUmbracoContextAccessor.cs rename to src/Umbraco.Core/Web/HybridUmbracoContextAccessor.cs diff --git a/src/Umbraco.Core/IUmbracoContext.cs b/src/Umbraco.Core/Web/IUmbracoContext.cs similarity index 100% rename from src/Umbraco.Core/IUmbracoContext.cs rename to src/Umbraco.Core/Web/IUmbracoContext.cs diff --git a/src/Umbraco.Core/IUmbracoContextAccessor.cs b/src/Umbraco.Core/Web/IUmbracoContextAccessor.cs similarity index 100% rename from src/Umbraco.Core/IUmbracoContextAccessor.cs rename to src/Umbraco.Core/Web/IUmbracoContextAccessor.cs diff --git a/src/Umbraco.Core/IUmbracoContextFactory.cs b/src/Umbraco.Core/Web/IUmbracoContextFactory.cs similarity index 100% rename from src/Umbraco.Core/IUmbracoContextFactory.cs rename to src/Umbraco.Core/Web/IUmbracoContextFactory.cs diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttributeTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttributeTests.cs index 3a987fb038..bf5c422bd8 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttributeTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttributeTests.cs @@ -16,6 +16,7 @@ using Moq; using NUnit.Framework; using Umbraco.Web.Models; using Umbraco.Web.Mvc; +using Umbraco.Web.Website.Controllers; namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Controllers { @@ -118,48 +119,48 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Controllers public ActionDescriptorCollection ActionDescriptors { get; } } - private class MatchesDefaultIndexController : RenderMvcController + private class MatchesDefaultIndexController : RenderController { - public MatchesDefaultIndexController(ILogger logger, + public MatchesDefaultIndexController(ILogger logger, ICompositeViewEngine compositeViewEngine) : base(logger, compositeViewEngine) { } } - private class MatchesOverriddenIndexController : RenderMvcController + private class MatchesOverriddenIndexController : RenderController { public override IActionResult Index(ContentModel model) { return base.Index(model); } - public MatchesOverriddenIndexController(ILogger logger, + public MatchesOverriddenIndexController(ILogger logger, ICompositeViewEngine compositeViewEngine) : base(logger, compositeViewEngine) { } } - private class MatchesCustomIndexController : RenderMvcController + private class MatchesCustomIndexController : RenderController { public IActionResult Index(ContentModel model, int page) { return base.Index(model); } - public MatchesCustomIndexController(ILogger logger, + public MatchesCustomIndexController(ILogger logger, ICompositeViewEngine compositeViewEngine) : base(logger, compositeViewEngine) { } } - private class MatchesAsyncIndexController : RenderMvcController + private class MatchesAsyncIndexController : RenderController { public new async Task Index(ContentModel model) { return await Task.FromResult(base.Index(model)); } - public MatchesAsyncIndexController(ILogger logger, + public MatchesAsyncIndexController(ILogger logger, ICompositeViewEngine compositeViewEngine) : base(logger, compositeViewEngine) { } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs index 82bd6719d4..d1ffc2044e 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/SurfaceControllerTests.cs @@ -19,6 +19,7 @@ using Umbraco.Web.Routing; using Umbraco.Web.Security; using Umbraco.Web.Website; using Umbraco.Web.Website.Controllers; +using Umbraco.Web.Website.Routing; using CoreConstants = Umbraco.Core.Constants; namespace Umbraco.Tests.UnitTests.Umbraco.Web.Website.Controllers diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs index 19fb6aa2df..34d3a96ca3 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs @@ -30,10 +30,10 @@ using Umbraco.Web.BackOffice.Security; using Umbraco.Web.Common.ActionsResults; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Authorization; +using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Filters; using Umbraco.Web.Common.Security; using Umbraco.Web.Models; -using Umbraco.Web.Mvc; using Umbraco.Web.WebAssets; using Constants = Umbraco.Core.Constants; diff --git a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs index 6ff42a5737..fe4951bc2b 100644 --- a/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.BackOffice/Extensions/BackOfficeApplicationBuilderExtensions.cs @@ -8,60 +8,49 @@ using Umbraco.Web.Common.Security; namespace Umbraco.Extensions { + /// + /// extensions for Umbraco + /// public static class BackOfficeApplicationBuilderExtensions { - public static IApplicationBuilder UseUmbraco(this IApplicationBuilder app) - { - if (app == null) throw new ArgumentNullException(nameof(app)); - app.UseStatusCodePages(); - app.UseRouting(); - - app.UseUmbracoCore(); - app.UseUmbracoRouting(); - app.UseRequestLocalization(); - app.UseUmbracoRequestLogging(); - app.UseUmbracoBackOffice(); - app.UseUmbracoPreview(); - app.UseUmbracoInstaller(); - - return app; - } - public static IApplicationBuilder UseUmbracoBackOffice(this IApplicationBuilder app) { - if (app == null) throw new ArgumentNullException(nameof(app)); + // NOTE: This method will have been called after UseRouting, UseAuthentication, UseAuthorization + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } app.UseBackOfficeUserManagerAuditing(); - // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. - // TODO: Since we are dependent on these we need to register them but what happens when we call this multiple times since we are dependent on this for UseUmbracoBackOffice too? - app.UseImageSharp(); - app.UseStaticFiles(); - - // Must be called after UseRouting and before UseEndpoints - app.UseSession(); - - if (!app.UmbracoCanBoot()) return app; + if (!app.UmbracoCanBoot()) + { + return app; + } app.UseEndpoints(endpoints => { - var backOfficeRoutes = app.ApplicationServices.GetRequiredService(); + BackOfficeAreaRoutes backOfficeRoutes = app.ApplicationServices.GetRequiredService(); backOfficeRoutes.CreateRoutes(endpoints); }); app.UseUmbracoRuntimeMinification(); - app.UseMiddleware(); app.UseMiddleware(); + app.UseUmbracoPreview(); + return app; } public static IApplicationBuilder UseUmbracoPreview(this IApplicationBuilder app) { + // TODO: I'm unsure this middleware will execute before the endpoint, we'll have to see + app.UseMiddleware(); + app.UseEndpoints(endpoints => { - var previewRoutes = app.ApplicationServices.GetRequiredService(); + PreviewRoutes previewRoutes = app.ApplicationServices.GetRequiredService(); previewRoutes.CreateRoutes(endpoints); }); diff --git a/src/Umbraco.Web.BackOffice/Middleware/PreviewAuthenticationMiddleware.cs b/src/Umbraco.Web.BackOffice/Middleware/PreviewAuthenticationMiddleware.cs index 284dbbc913..06715b4ad1 100644 --- a/src/Umbraco.Web.BackOffice/Middleware/PreviewAuthenticationMiddleware.cs +++ b/src/Umbraco.Web.BackOffice/Middleware/PreviewAuthenticationMiddleware.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -45,7 +45,7 @@ namespace Umbraco.Web.BackOffice.Middleware if (cookieOptions == null) throw new InvalidOperationException("No cookie options found with name " + Constants.Security.BackOfficeAuthenticationType); - //If we've gotten this far it means a preview cookie has been set and a front-end umbraco document request is executing. + // If we've gotten this far it means a preview cookie has been set and a front-end umbraco document request is executing. // In this case, authentication will not have occurred for an Umbraco back office User, however we need to perform the authentication // for the user here so that the preview capability can be authorized otherwise only the non-preview page will be rendered. if (request.Cookies.TryGetValue(cookieOptions.Cookie.Name, out var cookie)) @@ -55,7 +55,7 @@ namespace Umbraco.Web.BackOffice.Middleware { var backOfficeIdentity = unprotected.Principal.GetUmbracoIdentity(); if (backOfficeIdentity != null) - //Ok, we've got a real ticket, now we can add this ticket's identity to the current + // Ok, we've got a real ticket, now we can add this ticket's identity to the current // Principal, this means we'll have 2 identities assigned to the principal which we can // use to authorize the preview and allow for a back office User. diff --git a/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs index 2b9370fedd..000740e27e 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ApplicationTreeController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -46,13 +46,13 @@ namespace Umbraco.Web.BackOffice.Trees IControllerFactory controllerFactory, IActionDescriptorCollectionProvider actionDescriptorCollectionProvider ) - { + { _treeService = treeService; _sectionService = sectionService; _localizedTextService = localizedTextService; _controllerFactory = controllerFactory; _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; - } + } /// /// Returns the tree nodes for an application @@ -62,7 +62,7 @@ namespace Umbraco.Web.BackOffice.Trees /// /// Tree use. /// - public async Task GetApplicationTrees(string application, string tree, [ModelBinder(typeof(HttpQueryStringModelBinder))]FormCollection queryStrings, TreeUse use = TreeUse.Main) + public async Task GetApplicationTrees(string application, string tree, [ModelBinder(typeof(HttpQueryStringModelBinder))] FormCollection queryStrings, TreeUse use = TreeUse.Main) { application = application.CleanForXss(); @@ -165,7 +165,8 @@ namespace Umbraco.Web.BackOffice.Trees /// private async Task TryGetRootNode(Tree tree, FormCollection querystring) { - if (tree == null) throw new ArgumentNullException(nameof(tree)); + if (tree == null) + throw new ArgumentNullException(nameof(tree)); try { @@ -185,7 +186,8 @@ namespace Umbraco.Web.BackOffice.Trees /// private async Task GetTreeRootNode(Tree tree, int id, FormCollection querystring) { - if (tree == null) throw new ArgumentNullException(nameof(tree)); + if (tree == null) + throw new ArgumentNullException(nameof(tree)); var children = await GetChildren(tree, id, querystring); var rootNode = await GetRootNode(tree, querystring); @@ -214,9 +216,10 @@ namespace Umbraco.Web.BackOffice.Trees /// private async Task GetRootNode(Tree tree, FormCollection querystring) { - if (tree == null) throw new ArgumentNullException(nameof(tree)); + if (tree == null) + throw new ArgumentNullException(nameof(tree)); - var controller = (TreeControllerBase) await GetApiControllerProxy(tree.TreeControllerType, "GetRootNode", querystring); + var controller = (TreeControllerBase)await GetApiControllerProxy(tree.TreeControllerType, "GetRootNode", querystring); var rootNode = controller.GetRootNode(querystring); if (rootNode == null) throw new InvalidOperationException($"Failed to get root node for tree \"{tree.TreeAlias}\"."); @@ -228,7 +231,8 @@ namespace Umbraco.Web.BackOffice.Trees /// private async Task GetChildren(Tree tree, int id, FormCollection querystring) { - if (tree == null) throw new ArgumentNullException(nameof(tree)); + if (tree == null) + throw new ArgumentNullException(nameof(tree)); // the method we proxy has an 'id' parameter which is *not* in the querystring, // we need to add it for the proxy to work (else, it does not find the method, @@ -237,7 +241,7 @@ namespace Umbraco.Web.BackOffice.Trees d["id"] = StringValues.Empty; var proxyQuerystring = new FormCollection(d); - var controller = (TreeControllerBase) await GetApiControllerProxy(tree.TreeControllerType, "GetNodes", proxyQuerystring); + var controller = (TreeControllerBase)await GetApiControllerProxy(tree.TreeControllerType, "GetNodes", proxyQuerystring); return controller.GetNodes(id.ToInvariantString(), querystring); } @@ -267,7 +271,7 @@ namespace Umbraco.Web.BackOffice.Trees }); if (!(querystring is null)) { - foreach (var (key,value) in querystring) + foreach (var (key, value) in querystring) { routeData.Values[key] = value; } @@ -281,11 +285,11 @@ namespace Umbraco.Web.BackOffice.Trees var actionContext = new ActionContext(HttpContext, routeData, actionDescriptor); var proxyControllerContext = new ControllerContext(actionContext); - var controller = (TreeController) _controllerFactory.CreateController(proxyControllerContext); + var controller = (TreeController)_controllerFactory.CreateController(proxyControllerContext); - var isAllowed = await controller.ControllerContext.InvokeAuthorizationFiltersForRequest(actionContext); - if (!isAllowed) - throw new HttpResponseException(HttpStatusCode.Forbidden); + var isAllowed = await controller.ControllerContext.InvokeAuthorizationFiltersForRequest(actionContext); + if (!isAllowed) + throw new HttpResponseException(HttpStatusCode.Forbidden); return controller; } diff --git a/src/Umbraco.Web.Common/Controllers/IRenderController.cs b/src/Umbraco.Web.Common/Controllers/IRenderController.cs index 7534abc9b4..26a1286afa 100644 --- a/src/Umbraco.Web.Common/Controllers/IRenderController.cs +++ b/src/Umbraco.Web.Common/Controllers/IRenderController.cs @@ -1,9 +1,11 @@ -namespace Umbraco.Web.Mvc +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Common.Controllers { /// /// A marker interface to designate that a controller will be used for Umbraco front-end requests and/or route hijacking /// - public interface IRenderController + public interface IRenderController : IDiscoverable { } diff --git a/src/Umbraco.Web.Common/Controllers/IRenderMvcController.cs b/src/Umbraco.Web.Common/Controllers/IRenderMvcController.cs deleted file mode 100644 index 8727918bf4..0000000000 --- a/src/Umbraco.Web.Common/Controllers/IRenderMvcController.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Umbraco.Core.Composing; -using Umbraco.Web.Models; - -namespace Umbraco.Web.Mvc -{ - /// - /// The interface that must be implemented for a controller to be designated to execute for route hijacking - /// - public interface IRenderMvcController : IRenderController, IDiscoverable - { - /// - /// The default action to render the front-end view - /// - /// - /// - IActionResult Index(ContentModel model); - } -} diff --git a/src/Umbraco.Web.Common/Controllers/RenderController.cs b/src/Umbraco.Web.Common/Controllers/RenderController.cs deleted file mode 100644 index b95859ccbe..0000000000 --- a/src/Umbraco.Web.Common/Controllers/RenderController.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Umbraco.Web.Mvc; - -namespace Umbraco.Web.Common.Controllers -{ - public abstract class RenderController : Controller, IRenderController - { - - } -} diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoController.cs b/src/Umbraco.Web.Common/Controllers/UmbracoController.cs index 50ec620741..22bef0da69 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoController.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoController.cs @@ -1,14 +1,7 @@ -using System; +using System; using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Umbraco.Core.Cache; -using Umbraco.Core.Logging; -using Umbraco.Core.Configuration.Models; -using Umbraco.Core.Services; -using Umbraco.Web.Security; -namespace Umbraco.Web.Mvc +namespace Umbraco.Web.Common.Controllers { /// /// Provides a base class for Umbraco controllers. diff --git a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs index caf4132664..99a2b2aa3f 100644 --- a/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/ApplicationBuilderExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Serilog.Context; +using SixLabors.ImageSharp.Web.DependencyInjection; using Smidge; using Smidge.Nuglify; using StackExchange.Profiling; @@ -14,16 +15,61 @@ using Umbraco.Web.Common.Middleware; namespace Umbraco.Extensions { + /// + /// extensions for Umbraco + /// public static class ApplicationBuilderExtensions { + /// + /// Configures and use services required for using Umbraco + /// + public static IApplicationBuilder UseUmbraco(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + app.UseUmbracoCore(); + app.UseUmbracoRequestLogging(); + + // We need to add this before UseRouting so that the UmbracoContext and other middlewares are executed + // before endpoint routing middleware. + app.UseUmbracoRouting(); + + app.UseStatusCodePages(); + + // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. + // TODO: Since we are dependent on these we need to register them but what happens when we call this multiple times since we are dependent on this for UseUmbracoBackOffice too? + app.UseImageSharp(); + app.UseStaticFiles(); + + // UseRouting adds endpoint routing middleware, this means that middlewares registered after this one + // will execute after endpoint routing. The ordering of everything is quite important here, see + // https://docs.microsoft.com/en-us/aspnet/core/fundamentals/routing?view=aspnetcore-5.0 + // where we need to have UseAuthentication and UseAuthorization proceeding this call but before + // endpoints are defined. + app.UseRouting(); + app.UseRequestLocalization(); + app.UseAuthentication(); + app.UseAuthorization(); + + // Must be called after UseRouting and before UseEndpoints + app.UseSession(); + + // Must come after the above! + app.UseUmbracoInstaller(); + + return app; + } + /// /// Returns true if Umbraco is greater than /// - /// - /// public static bool UmbracoCanBoot(this IApplicationBuilder app) { - var runtime = app.ApplicationServices.GetRequiredService(); + IRuntime runtime = app.ApplicationServices.GetRequiredService(); + // can't continue if boot failed return runtime.State.Level > RuntimeLevel.BootFailed; } @@ -31,26 +77,30 @@ namespace Umbraco.Extensions /// /// Start Umbraco /// - /// - /// public static IApplicationBuilder UseUmbracoCore(this IApplicationBuilder app) { - if (app == null) throw new ArgumentNullException(nameof(app)); + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } - if (!app.UmbracoCanBoot()) return app; + if (!app.UmbracoCanBoot()) + { + return app; + } - var hostingEnvironment = app.ApplicationServices.GetRequiredService(); - AppDomain.CurrentDomain.SetData("DataDirectory", hostingEnvironment?.MapPathContentRoot(Core.Constants.SystemDirectories.Data)); + IHostingEnvironment hostingEnvironment = app.ApplicationServices.GetRequiredService(); + AppDomain.CurrentDomain.SetData("DataDirectory", hostingEnvironment?.MapPathContentRoot(Constants.SystemDirectories.Data)); - var runtime = app.ApplicationServices.GetRequiredService(); + IRuntime runtime = app.ApplicationServices.GetRequiredService(); // Register a listener for application shutdown in order to terminate the runtime - var hostLifetime = app.ApplicationServices.GetRequiredService(); + IApplicationShutdownRegistry hostLifetime = app.ApplicationServices.GetRequiredService(); var runtimeShutdown = new CoreRuntimeShutdown(runtime, hostLifetime); hostLifetime.RegisterObject(runtimeShutdown); // Register our global threadabort enricher for logging - var threadAbortEnricher = app.ApplicationServices.GetRequiredService(); + ThreadAbortExceptionEnricher threadAbortEnricher = app.ApplicationServices.GetRequiredService(); LogContext.Push(threadAbortEnricher); // NOTE: We are not in a using clause because we are not removing it, it is on the global context StaticApplicationLogging.Initialize(app.ApplicationServices.GetRequiredService()); @@ -64,12 +114,16 @@ namespace Umbraco.Extensions /// /// Enables middlewares required to run Umbraco /// - /// - /// - // TODO: Could be internal or part of another call - this is a required system so should't be 'opt-in' + /// + /// Must occur before UseRouting + /// public static IApplicationBuilder UseUmbracoRouting(this IApplicationBuilder app) { - if (app == null) throw new ArgumentNullException(nameof(app)); + // TODO: This method could be internal or part of another call - this is a required system so should't be 'opt-in' + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } if (!app.UmbracoCanBoot()) { @@ -79,11 +133,6 @@ namespace Umbraco.Extensions { app.UseMiddleware(); app.UseMiddleware(); - - // TODO: Both of these need to be done before any endpoints but after UmbracoRequestMiddleware - // because they rely on an UmbracoContext. But should they be here? - app.UseAuthentication(); - app.UseAuthorization(); } return app; @@ -92,11 +141,12 @@ namespace Umbraco.Extensions /// /// Adds request based serilog enrichers to the LogContext for each request /// - /// - /// public static IApplicationBuilder UseUmbracoRequestLogging(this IApplicationBuilder app) { - if (app == null) throw new ArgumentNullException(nameof(app)); + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } if (!app.UmbracoCanBoot()) return app; @@ -108,13 +158,17 @@ namespace Umbraco.Extensions /// /// Enables runtime minification for Umbraco /// - /// - /// public static IApplicationBuilder UseUmbracoRuntimeMinification(this IApplicationBuilder app) { - if (app == null) throw new ArgumentNullException(nameof(app)); + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } - if (!app.UmbracoCanBoot()) return app; + if (!app.UmbracoCanBoot()) + { + return app; + } app.UseSmidge(); app.UseSmidgeNuglify(); diff --git a/src/Umbraco.Web.Common/Extensions/UmbracoInstallApplicationBuilderExtensions.cs b/src/Umbraco.Web.Common/Extensions/UmbracoInstallApplicationBuilderExtensions.cs index 3350af756e..ac5d787911 100644 --- a/src/Umbraco.Web.Common/Extensions/UmbracoInstallApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UmbracoInstallApplicationBuilderExtensions.cs @@ -1,30 +1,31 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Umbraco.Web.Common.Install; namespace Umbraco.Extensions { + /// + /// extensions for Umbraco installer + /// public static class UmbracoInstallApplicationBuilderExtensions { /// /// Enables the Umbraco installer /// - /// - /// public static IApplicationBuilder UseUmbracoInstaller(this IApplicationBuilder app) { - if (!app.UmbracoCanBoot()) return app; + if (!app.UmbracoCanBoot()) + { + return app; + } app.UseEndpoints(endpoints => { - var installerRoutes = app.ApplicationServices.GetRequiredService(); + InstallAreaRoutes installerRoutes = app.ApplicationServices.GetRequiredService(); installerRoutes.CreateRoutes(endpoints); }); return app; } - - } - } diff --git a/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs index 360396fe04..c53c367689 100644 --- a/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs +++ b/src/Umbraco.Web.Common/Filters/EnsurePartialViewMacroViewContextFilterAttribute.cs @@ -1,4 +1,4 @@ -using System.IO; +using System.IO; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; @@ -7,7 +7,6 @@ using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Umbraco.Web.Common.Constants; using Umbraco.Web.Common.Controllers; -using Umbraco.Web.Mvc; namespace Umbraco.Web.Common.Filters { @@ -35,13 +34,18 @@ namespace Umbraco.Web.Common.Filters /// this ensures that any calls to GetPropertyValue with regards to RTE or Grid editors can still /// render any PartialViewMacro with a form and maintain ModelState /// - /// public override void OnActionExecuting(ActionExecutingContext context) { - if (!(context.Controller is Controller controller)) return; + if (!(context.Controller is Controller controller)) + { + return; + } - //ignore anything that is not IRenderController - if (!(controller is IRenderController)) return; + // ignore anything that is not IRenderController + if (!(controller is IRenderController)) + { + return; + } SetViewContext(context, controller); } @@ -54,10 +58,16 @@ namespace Umbraco.Web.Common.Filters /// The filter context. public override void OnResultExecuting(ResultExecutingContext context) { - if (!(context.Controller is Controller controller)) return; + if (!(context.Controller is Controller controller)) + { + return; + } - //ignore anything that is not IRenderController - if (!(controller is RenderController)) return; + // ignore anything that is not IRenderController + if (!(controller is IRenderController)) + { + return; + } SetViewContext(context, controller); } @@ -72,16 +82,13 @@ namespace Umbraco.Web.Common.Filters new StringWriter(), new HtmlHelperOptions()); - //set the special data token + // set the special data token context.RouteData.DataTokens[ViewConstants.DataTokenCurrentViewContext] = viewCtx; } private class DummyView : IView { - public Task RenderAsync(ViewContext context) - { - return Task.CompletedTask; - } + public Task RenderAsync(ViewContext context) => Task.CompletedTask; public string Path { get; } } diff --git a/src/Umbraco.Web.Common/Filters/PreRenderViewActionFilterAttribute.cs b/src/Umbraco.Web.Common/Filters/PreRenderViewActionFilterAttribute.cs deleted file mode 100644 index 2ba58a8fd7..0000000000 --- a/src/Umbraco.Web.Common/Filters/PreRenderViewActionFilterAttribute.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.Filters; -using Umbraco.Web.Common.Events; - -namespace Umbraco.Web.Common.Filters -{ - public class PreRenderViewActionFilterAttribute : ActionFilterAttribute - { - public override void OnActionExecuted(ActionExecutedContext context) - { - if (!(context.Controller is Controller umbController) || !(context.Result is ViewResult result)) - { - return; - } - - var model = result.Model; - if (model == null) - { - return; - } - - var args = new ActionExecutedEventArgs(umbController, model); - OnActionExecuted(args); - - if (args.Model != model) - { - result.ViewData.Model = args.Model; - } - - base.OnActionExecuted(context); - } - - - public static event EventHandler ActionExecuted; - - private static void OnActionExecuted(ActionExecutedEventArgs e) - { - var handler = ActionExecuted; - handler?.Invoke(null, e); - } - } -} diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs index e274e479b7..6b5d305a64 100644 --- a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs @@ -1,15 +1,13 @@ -using System; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.Extensions.Logging; -using Umbraco.Web.Common.Lifetime; using Umbraco.Core; -using Umbraco.Core.Logging; -using System.Threading; using Umbraco.Core.Cache; -using System.Collections.Generic; -using Umbraco.Core.Security; +using Umbraco.Core.Logging; +using Umbraco.Web.Common.Lifetime; namespace Umbraco.Web.Common.Middleware { @@ -28,6 +26,9 @@ namespace Umbraco.Web.Common.Middleware private readonly IRequestCache _requestCache; private readonly IBackOfficeSecurityFactory _backofficeSecurityFactory; + /// + /// Initializes a new instance of the class. + /// public UmbracoRequestMiddleware( ILogger logger, IUmbracoRequestLifetimeManager umbracoRequestLifetimeManager, @@ -42,6 +43,7 @@ namespace Umbraco.Web.Common.Middleware _backofficeSecurityFactory = backofficeSecurityFactory; } + /// public async Task InvokeAsync(HttpContext context, RequestDelegate next) { var requestUri = new Uri(context.Request.GetEncodedUrl(), UriKind.RelativeOrAbsolute); @@ -52,16 +54,16 @@ namespace Umbraco.Web.Common.Middleware await next(context); return; } - _backofficeSecurityFactory.EnsureBackOfficeSecurity(); // Needs to be before UmbracoContext - var umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); + _backofficeSecurityFactory.EnsureBackOfficeSecurity(); // Needs to be before UmbracoContext + UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); try { if (umbracoContextReference.UmbracoContext.IsFrontEndUmbracoRequest) { - LogHttpRequest.TryGetCurrentHttpRequestId(out var httpRequestId, _requestCache); - _logger.LogTrace("Begin request [{HttpRequestId}]: {RequestUrl}", httpRequestId, requestUri); + LogHttpRequest.TryGetCurrentHttpRequestId(out Guid httpRequestId, _requestCache); + _logger.LogTrace("Begin request [{HttpRequestId}]: {RequestUrl}", httpRequestId, requestUri); } try diff --git a/src/Umbraco.Web.Common/Routing/IAreaRoutes.cs b/src/Umbraco.Web.Common/Routing/IAreaRoutes.cs index b561abc4dd..b01f703016 100644 --- a/src/Umbraco.Web.Common/Routing/IAreaRoutes.cs +++ b/src/Umbraco.Web.Common/Routing/IAreaRoutes.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing; namespace Umbraco.Web.Common.Routing { @@ -11,6 +11,10 @@ namespace Umbraco.Web.Common.Routing // on individual ext methods. This would reduce the amount of code in Startup, but could also mean there's less control over startup // if someone wanted that. Maybe we can just have both. + /// + /// Create routes for an area + /// + /// The endpoint route builder void CreateRoutes(IEndpointRouteBuilder endpoints); } } diff --git a/src/Umbraco.Web.UI.NetCore/Startup.cs b/src/Umbraco.Web.UI.NetCore/Startup.cs index d496aadfd3..6b57c8af9d 100644 --- a/src/Umbraco.Web.UI.NetCore/Startup.cs +++ b/src/Umbraco.Web.UI.NetCore/Startup.cs @@ -54,6 +54,7 @@ namespace Umbraco.Web.UI.NetCore } app.UseUmbraco(); + app.UseUmbracoBackOffice(); app.UseUmbracoWebsite(); } } diff --git a/src/Umbraco.Web.Website/Controllers/IUmbracoRenderingDefaults.cs b/src/Umbraco.Web.Website/Controllers/IUmbracoRenderingDefaults.cs new file mode 100644 index 0000000000..507b8c4a04 --- /dev/null +++ b/src/Umbraco.Web.Website/Controllers/IUmbracoRenderingDefaults.cs @@ -0,0 +1,15 @@ +using System; + +namespace Umbraco.Web.Website.Controllers +{ + /// + /// The defaults used for rendering Umbraco front-end pages + /// + public interface IUmbracoRenderingDefaults + { + /// + /// Gets the default umbraco render controller type + /// + Type DefaultControllerType { get; } + } +} diff --git a/src/Umbraco.Web.Website/Controllers/RenderMvcController.cs b/src/Umbraco.Web.Website/Controllers/RenderController.cs similarity index 78% rename from src/Umbraco.Web.Website/Controllers/RenderMvcController.cs rename to src/Umbraco.Web.Website/Controllers/RenderController.cs index 62ffb010ea..071560d860 100644 --- a/src/Umbraco.Web.Website/Controllers/RenderMvcController.cs +++ b/src/Umbraco.Web.Website/Controllers/RenderController.cs @@ -3,25 +3,28 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.Extensions.Logging; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.Common.Controllers; using Umbraco.Web.Common.Filters; using Umbraco.Web.Models; using Umbraco.Web.Routing; -namespace Umbraco.Web.Mvc +namespace Umbraco.Web.Website.Controllers { /// /// Represents the default front-end rendering controller. /// - [PreRenderViewActionFilter] [TypeFilter(typeof(ModelBindingExceptionFilter))] - public class RenderMvcController : UmbracoController, IRenderMvcController + public class RenderController : UmbracoController, IRenderController { private IPublishedRequest _publishedRequest; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly ICompositeViewEngine _compositeViewEngine; - public RenderMvcController(ILogger logger, ICompositeViewEngine compositeViewEngine) + /// + /// Initializes a new instance of the class. + /// + public RenderController(ILogger logger, ICompositeViewEngine compositeViewEngine) { _logger = logger; _compositeViewEngine = compositeViewEngine; @@ -40,11 +43,15 @@ namespace Umbraco.Web.Mvc get { if (_publishedRequest != null) + { return _publishedRequest; + } + if (RouteData.DataTokens.ContainsKey(Core.Constants.Web.PublishedDocumentRequestDataToken) == false) { throw new InvalidOperationException("DataTokens must contain an 'umbraco-doc-request' key with a PublishedRequest object"); } + _publishedRequest = (IPublishedRequest)RouteData.DataTokens[Core.Constants.Web.PublishedDocumentRequestDataToken]; return _publishedRequest; } @@ -56,8 +63,11 @@ namespace Umbraco.Web.Mvc /// The view name. protected bool EnsurePhsyicalViewExists(string template) { - var result = _compositeViewEngine.FindView(ControllerContext, template, false); - if (result.View != null) return true; + ViewEngineResult result = _compositeViewEngine.FindView(ControllerContext, template, false); + if (result.View != null) + { + return true; + } _logger.LogWarning("No physical template file was found for template {Template}", template); return false; @@ -74,19 +84,17 @@ namespace Umbraco.Web.Mvc { var template = ControllerContext.RouteData.Values["action"].ToString(); if (EnsurePhsyicalViewExists(template) == false) + { throw new InvalidOperationException("No physical template file was found for template " + template); + } + return View(template, model); } /// /// The default action to render the front-end view. /// - /// - /// [RenderIndexActionSelector] - public virtual IActionResult Index(ContentModel model) - { - return CurrentTemplate(model); - } + public virtual IActionResult Index(ContentModel model) => CurrentTemplate(model); } } diff --git a/src/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttribute.cs b/src/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttribute.cs index f1ea65e983..0027132c23 100644 --- a/src/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttribute.cs +++ b/src/Umbraco.Web.Website/Controllers/RenderIndexActionSelectorAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; @@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; -namespace Umbraco.Web.Mvc +namespace Umbraco.Web.Website.Controllers { /// /// A custom ActionMethodSelector which will ensure that the RenderMvcController.Index(ContentModel model) action will be executed @@ -35,17 +35,18 @@ namespace Umbraco.Web.Mvc var baseType = controllerAction.ControllerTypeInfo.BaseType; //It's the same type, so this must be the Index action to use - if (currType == baseType) return true; + if (currType == baseType) + return true; - var actions = _controllerActionsCache.GetOrAdd(currType, type => - { - var actionDescriptors = routeContext.HttpContext.RequestServices - .GetRequiredService().ActionDescriptors.Items - .Where(x=>x is ControllerActionDescriptor).Cast() - .Where(x => x.ControllerTypeInfo == controllerAction.ControllerTypeInfo); + var actions = _controllerActionsCache.GetOrAdd(currType, type => + { + var actionDescriptors = routeContext.HttpContext.RequestServices + .GetRequiredService().ActionDescriptors.Items + .Where(x => x is ControllerActionDescriptor).Cast() + .Where(x => x.ControllerTypeInfo == controllerAction.ControllerTypeInfo); - return actionDescriptors; - }); + return actionDescriptors; + }); //If there are more than one Index action for this controller, then // this base class one should not be matched diff --git a/src/Umbraco.Web.Website/Controllers/SurfaceController.cs b/src/Umbraco.Web.Website/Controllers/SurfaceController.cs index 4e0517754c..1d3e4c5626 100644 --- a/src/Umbraco.Web.Website/Controllers/SurfaceController.cs +++ b/src/Umbraco.Web.Website/Controllers/SurfaceController.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Specialized; using Microsoft.AspNetCore.Http; using Umbraco.Core; @@ -10,6 +10,7 @@ using Umbraco.Core.Services; using Umbraco.Web.Common.Controllers; using Umbraco.Web.Routing; using Umbraco.Web.Website.ActionResults; +using Umbraco.Web.Website.Routing; namespace Umbraco.Web.Website.Controllers { diff --git a/src/Umbraco.Web.Website/Controllers/UmbracoRenderingDefaults.cs b/src/Umbraco.Web.Website/Controllers/UmbracoRenderingDefaults.cs new file mode 100644 index 0000000000..669e1835d4 --- /dev/null +++ b/src/Umbraco.Web.Website/Controllers/UmbracoRenderingDefaults.cs @@ -0,0 +1,13 @@ +using System; + +namespace Umbraco.Web.Website.Controllers +{ + /// + /// The defaults used for rendering Umbraco front-end pages + /// + public class UmbracoRenderingDefaults : IUmbracoRenderingDefaults + { + /// + public Type DefaultControllerType => typeof(RenderController); + } +} diff --git a/src/Umbraco.Web.Website/Extensions/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/Extensions/UmbracoBuilderExtensions.cs index 72d12809e2..cbfa0c659e 100644 --- a/src/Umbraco.Web.Website/Extensions/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/UmbracoBuilderExtensions.cs @@ -1,14 +1,21 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Core.DependencyInjection; -using Umbraco.Core.DependencyInjection; +using Umbraco.Web.Website.Controllers; +using Umbraco.Web.Website.Routing; using Umbraco.Web.Website.ViewEngines; namespace Umbraco.Extensions { + /// + /// extensions for umbraco front-end website + /// public static class UmbracoBuilderExtensions { + /// + /// Add services for the umbraco front-end website + /// public static IUmbracoBuilder AddUmbracoWebsite(this IUmbracoBuilder builder) { // Set the render & plugin view engines (Super complicated, but this allows us to use the IServiceCollection @@ -21,12 +28,14 @@ namespace Umbraco.Extensions // Wraps all existing view engines in a ProfilerViewEngine builder.Services.AddTransient, ProfilingViewEngineWrapperMvcViewOptionsSetup>(); - //TODO figure out if we need more to work on load balanced setups + // TODO figure out if we need more to work on load balanced setups builder.Services.AddDataProtection(); + builder.Services.AddScoped(); + builder.Services.AddSingleton(); + return builder; } - } } diff --git a/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs b/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs index ef99d67373..32d84088c1 100644 --- a/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/UmbracoWebsiteApplicationBuilderExtensions.cs @@ -6,28 +6,41 @@ using Umbraco.Web.Website.Routing; namespace Umbraco.Extensions { + /// + /// extensions for the umbraco front-end website + /// public static class UmbracoWebsiteApplicationBuilderExtensions { + /// + /// Sets up services and routes for the front-end umbraco website + /// public static IApplicationBuilder UseUmbracoWebsite(this IApplicationBuilder app) { - if (app == null) throw new ArgumentNullException(nameof(app)); + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } - if (!app.UmbracoCanBoot()) return app; + if (!app.UmbracoCanBoot()) + { + return app; + } - // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. - // TODO: Since we are dependent on these we need to register them but what happens when we call this multiple times since we are dependent on this for UseUmbracoBackOffice too? - app.UseImageSharp(); - app.UseStaticFiles(); - app.UseUmbracoNoContentPage(); + app.UseUmbracoRoutes(); return app; } - public static IApplicationBuilder UseUmbracoNoContentPage(this IApplicationBuilder app) + /// + /// Sets up routes for the umbraco front-end + /// + public static IApplicationBuilder UseUmbracoRoutes(this IApplicationBuilder app) { app.UseEndpoints(endpoints => { - var noContentRoutes = app.ApplicationServices.GetRequiredService(); + endpoints.MapDynamicControllerRoute("/{**slug}"); + + NoContentRoutes noContentRoutes = app.ApplicationServices.GetRequiredService(); noContentRoutes.CreateRoutes(endpoints); }); diff --git a/src/Umbraco.Web.Website/Routing/NoContentRoutes.cs b/src/Umbraco.Web.Website/Routing/NoContentRoutes.cs index 58885bcd96..f2f2e8dfe3 100644 --- a/src/Umbraco.Web.Website/Routing/NoContentRoutes.cs +++ b/src/Umbraco.Web.Website/Routing/NoContentRoutes.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; using Umbraco.Core; @@ -17,6 +17,9 @@ namespace Umbraco.Web.Website.Routing private readonly IRuntimeState _runtimeState; private readonly string _umbracoPathSegment; + /// + /// Initializes a new instance of the class. + /// public NoContentRoutes( IOptions globalSettings, IHostingEnvironment hostingEnvironment, @@ -26,6 +29,7 @@ namespace Umbraco.Web.Website.Routing _umbracoPathSegment = globalSettings.Value.GetUmbracoMvcArea(hostingEnvironment); } + /// public void CreateRoutes(IEndpointRouteBuilder endpoints) { switch (_runtimeState.Level) @@ -35,13 +39,16 @@ namespace Umbraco.Web.Website.Routing case RuntimeLevel.Upgrade: break; case RuntimeLevel.Run: + + // TODO: I don't really think this is working AFAIK the code has just been migrated but it's not really enabled + // yet. Our route handler needs to be aware that there is no content and redirect there. Though, this could all be + // managed directly in UmbracoRouteValueTransformer. Else it could actually do a 'redirect' but that would need to be + // an internal rewrite. endpoints.MapControllerRoute( - // named consistently - Constants.Web.NoContentRouteName, + Constants.Web.NoContentRouteName, // named consistently _umbracoPathSegment + "/UmbNoContent", - new { controller = "RenderNoContent", action = "Index" } - ); - break; + new { controller = "RenderNoContent", action = "Index" }); + break; case RuntimeLevel.BootFailed: case RuntimeLevel.Unknown: case RuntimeLevel.Boot: diff --git a/src/Umbraco.Web.Website/RouteDefinition.cs b/src/Umbraco.Web.Website/Routing/RouteDefinition.cs similarity index 94% rename from src/Umbraco.Web.Website/RouteDefinition.cs rename to src/Umbraco.Web.Website/Routing/RouteDefinition.cs index 02eab6ae77..47206bd0c3 100644 --- a/src/Umbraco.Web.Website/RouteDefinition.cs +++ b/src/Umbraco.Web.Website/Routing/RouteDefinition.cs @@ -1,7 +1,7 @@ -using System; +using System; using Umbraco.Web.Routing; -namespace Umbraco.Web.Website +namespace Umbraco.Web.Website.Routing { /// /// Represents the data required to route to a specific controller/action during an Umbraco request diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs new file mode 100644 index 0000000000..a6582f03ae --- /dev/null +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -0,0 +1,230 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Logging; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Strings; +using Umbraco.Extensions; +using Umbraco.Web.Common.Controllers; +using Umbraco.Web.Common.Middleware; +using Umbraco.Web.Models; +using Umbraco.Web.Routing; +using Umbraco.Web.Website.Controllers; + +namespace Umbraco.Web.Website.Routing +{ + /// + /// The route value transformer for Umbraco front-end routes + /// + public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer + { + private readonly ILogger _logger; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IUmbracoRenderingDefaults _renderingDefaults; + private readonly IShortStringHelper _shortStringHelper; + private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider; + private readonly IPublishedRouter _publishedRouter; + + /// + /// Initializes a new instance of the class. + /// + public UmbracoRouteValueTransformer( + ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoRenderingDefaults renderingDefaults, + IShortStringHelper shortStringHelper, + IActionDescriptorCollectionProvider actionDescriptorCollectionProvider, + IPublishedRouter publishedRouter) + { + _logger = logger; + _umbracoContextAccessor = umbracoContextAccessor; + _renderingDefaults = renderingDefaults; + _shortStringHelper = shortStringHelper; + _actionDescriptorCollectionProvider = actionDescriptorCollectionProvider; + _publishedRouter = publishedRouter; + } + + /// + public override async ValueTask TransformAsync(HttpContext httpContext, RouteValueDictionary values) + { + if (_umbracoContextAccessor.UmbracoContext == null) + { + throw new InvalidOperationException($"There is no current UmbracoContext, it must be initialized before the {nameof(UmbracoRouteValueTransformer)} executes, ensure that {nameof(UmbracoRequestMiddleware)} is registered prior to 'UseRouting'"); + } + + bool routed = RouteRequest(_umbracoContextAccessor.UmbracoContext); + if (!routed) + { + // TODO: Deal with it not being routable, perhaps this should be an enum result? + } + + IPublishedRequest request = _umbracoContextAccessor.UmbracoContext.PublishedRequest; // This cannot be null here + + SetupRouteDataForRequest( + new ContentModel(request.PublishedContent), + request, + values); + + RouteDefinition routeDef = GetUmbracoRouteDefinition(httpContext, values, request); + values["controller"] = routeDef.ControllerName; + if (string.IsNullOrWhiteSpace(routeDef.ActionName) == false) + { + values["action"] = routeDef.ActionName; + } + + return await Task.FromResult(values); + } + + /// + /// Ensures that all of the correct DataTokens are added to the route values which are all required for rendering front-end umbraco views + /// + private void SetupRouteDataForRequest(ContentModel contentModel, IPublishedRequest frequest, RouteValueDictionary values) + { + // put essential data into the data tokens, the 'umbraco' key is required to be there for the view engine + + // required for the ContentModelBinder and view engine. + // TODO: Are we sure, seems strange to need this in netcore + values.TryAdd(Constants.Web.UmbracoDataToken, contentModel); + + // required for RenderMvcController + // TODO: Are we sure, seems strange to need this in netcore + values.TryAdd(Constants.Web.PublishedDocumentRequestDataToken, frequest); + + // required for UmbracoViewPage + // TODO: Are we sure, seems strange to need this in netcore + values.TryAdd(Constants.Web.UmbracoContextDataToken, _umbracoContextAccessor.UmbracoContext); + } + + /// + /// Returns a object based on the current content request + /// + private RouteDefinition GetUmbracoRouteDefinition(HttpContext httpContext, RouteValueDictionary values, IPublishedRequest request) + { + if (httpContext is null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + if (values is null) + { + throw new ArgumentNullException(nameof(values)); + } + + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + + Type defaultControllerType = _renderingDefaults.DefaultControllerType; + var defaultControllerName = ControllerExtensions.GetControllerName(defaultControllerType); + + // creates the default route definition which maps to the 'UmbracoController' controller + var def = new RouteDefinition + { + ControllerName = defaultControllerName, + ControllerType = defaultControllerType, + PublishedRequest = request, + + // ActionName = ((Route)requestContext.RouteData.Route).Defaults["action"].ToString(), + ActionName = "Index", + HasHijackedRoute = false + }; + + // check that a template is defined), if it doesn't and there is a hijacked route it will just route + // to the index Action + if (request.HasTemplate) + { + // the template Alias should always be already saved with a safe name. + // if there are hyphens in the name and there is a hijacked route, then the Action will need to be attributed + // with the action name attribute. + var templateName = request.TemplateAlias.Split('.')[0].ToSafeAlias(_shortStringHelper); + def.ActionName = templateName; + } + + // check if there's a custom controller assigned, base on the document type alias. + Type controllerType = FindControllerType(request.PublishedContent.ContentType.Alias); + + // check if that controller exists + if (controllerType != null) + { + // ensure the controller is of type IRenderController and ControllerBase + if (TypeHelper.IsTypeAssignableFrom(controllerType) + && TypeHelper.IsTypeAssignableFrom(controllerType)) + { + // set the controller and name to the custom one + def.ControllerType = controllerType; + def.ControllerName = ControllerExtensions.GetControllerName(controllerType); + if (def.ControllerName != defaultControllerName) + { + def.HasHijackedRoute = true; + } + } + else + { + _logger.LogWarning( + "The current Document Type {ContentTypeAlias} matches a locally declared controller of type {ControllerName}. Custom Controllers for Umbraco routing must implement '{UmbracoRenderController}' and inherit from '{UmbracoControllerBase}'.", + request.PublishedContent.ContentType.Alias, + controllerType.FullName, + typeof(IRenderController).FullName, + typeof(ControllerBase).FullName); + + // we cannot route to this custom controller since it is not of the correct type so we'll continue with the defaults + // that have already been set above. + } + } + + // store the route definition + values.TryAdd(Constants.Web.UmbracoRouteDefinitionDataToken, def); + + return def; + } + + private Type FindControllerType(string controllerName) + { + ControllerActionDescriptor descriptor = _actionDescriptorCollectionProvider.ActionDescriptors.Items + .Cast() + .First(x => + x.ControllerName.Equals(controllerName)); + + return descriptor?.ControllerTypeInfo; + } + + private bool RouteRequest(IUmbracoContext umbracoContext) + { + // TODO: I suspect one day this will be async + + // ok, process + + // note: requestModule.UmbracoRewrite also did some stripping of &umbPage + // from the querystring... that was in v3.x to fix some issues with pre-forms + // auth. Paul Sterling confirmed in Jan. 2013 that we can get rid of it. + + // instantiate, prepare and process the published content request + // important to use CleanedUmbracoUrl - lowercase path-only version of the current url + IPublishedRequest request = _publishedRouter.CreateRequest(umbracoContext); + umbracoContext.PublishedRequest = request; // TODO: This is ugly + bool prepared = _publishedRouter.PrepareRequest(request); + return prepared && request.HasPublishedContent; + + // // HandleHttpResponseStatus returns a value indicating that the request should + // // not be processed any further, eg because it has been redirect. then, exit. + // if (UmbracoModule.HandleHttpResponseStatus(httpContext, request, _logger)) + // return; + // if (!request.HasPublishedContent == false) + // { + // // httpContext.RemapHandler(new PublishedContentNotFoundHandler()); + // } + // else + // { + // // RewriteToUmbracoHandler(httpContext, request); + // } + } + } +} diff --git a/src/Umbraco.Web/Mvc/RenderControllerFactory.cs b/src/Umbraco.Web/Mvc/RenderControllerFactory.cs deleted file mode 100644 index 091ace4d98..0000000000 --- a/src/Umbraco.Web/Mvc/RenderControllerFactory.cs +++ /dev/null @@ -1,45 +0,0 @@ -// using System.Web.Mvc; -// using System.Web.Routing; -// -// namespace Umbraco.Web.Mvc -// { -// /// -// /// A controller factory for the render pipeline of Umbraco. This controller factory tries to create a controller with the supplied -// /// name, and falls back to UmbracoController if none was found. -// /// -// /// -// public class RenderControllerFactory : UmbracoControllerFactory -// { -// /// -// /// Determines whether this instance can handle the specified request. -// /// -// /// The request. -// /// true if this instance can handle the specified request; otherwise, false. -// /// -// public override bool CanHandle(RequestContext request) -// { -// var dataToken = request.RouteData.DataTokens["area"]; -// return dataToken == null || string.IsNullOrWhiteSpace(dataToken.ToString()); -// } -// -// /// -// /// Creates the controller -// /// -// /// -// /// -// /// -// /// -// /// We always set the correct ActionInvoker on our custom created controller, this is very important for route hijacking! -// /// -// public override IController CreateController(RequestContext requestContext, string controllerName) -// { -// var instance = base.CreateController(requestContext, controllerName); -// if (instance is Controller controllerInstance) -// { -// //set the action invoker! -// controllerInstance.ActionInvoker = new RenderActionInvoker(); -// } -// return instance; -// } -// } -// } diff --git a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs index 163364602d..19e1b79c89 100644 --- a/src/Umbraco.Web/Mvc/RenderRouteHandler.cs +++ b/src/Umbraco.Web/Mvc/RenderRouteHandler.cs @@ -338,10 +338,10 @@ namespace Umbraco.Web.Mvc } - //Here we need to check if there is no hijacked route and no template assigned, - //if this is the case we want to return a blank page, but we'll leave that up to the NoTemplateHandler. - //We also check if templates have been disabled since if they are then we're allowed to render even though there's no template, - //for example for json rendering in headless. + // Here we need to check if there is no hijacked route and no template assigned, + // if this is the case we want to return a blank page, but we'll leave that up to the NoTemplateHandler. + // We also check if templates have been disabled since if they are then we're allowed to render even though there's no template, + // for example for json rendering in headless. if ((request.HasTemplate == false && Features.Disabled.DisableTemplates == false) && routeDef.HasHijackedRoute == false) { @@ -370,7 +370,7 @@ namespace Umbraco.Web.Mvc routeDef = GetUmbracoRouteDefinition(requestContext, request); } - //no post values, just route to the controller/action required (local) + // no post values, just route to the controller/action required (local) requestContext.RouteData.Values["controller"] = routeDef.ControllerName; if (string.IsNullOrWhiteSpace(routeDef.ActionName) == false) diff --git a/src/Umbraco.Web/Mvc/RouteDefinition.cs b/src/Umbraco.Web/Mvc/RouteDefinition.cs index 45e759fd66..2977c49cb5 100644 --- a/src/Umbraco.Web/Mvc/RouteDefinition.cs +++ b/src/Umbraco.Web/Mvc/RouteDefinition.cs @@ -1,13 +1,9 @@ -using System; -using System.Web.Mvc; +using System; using Umbraco.Web.Routing; namespace Umbraco.Web.Mvc { - /// - /// Represents the data required to route to a specific controller/action during an Umbraco request - /// - /// Migrated already to .Net Core + // TODO: Migrated already to .Net Core public class RouteDefinition { public string ControllerName { get; set; } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index ad7dd86730..8867971657 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -241,7 +241,6 @@ - diff --git a/src/Umbraco.Web/UmbracoInjectedModule.cs b/src/Umbraco.Web/UmbracoInjectedModule.cs index e9a1cc1436..5c7468ce95 100644 --- a/src/Umbraco.Web/UmbracoInjectedModule.cs +++ b/src/Umbraco.Web/UmbracoInjectedModule.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Web; using System.Web.Routing; using Microsoft.Extensions.Logging; @@ -157,9 +157,6 @@ namespace Umbraco.Web /// /// Checks the current request and ensures that it is routable based on the structure of the request and URI /// - /// - /// - /// internal Attempt EnsureUmbracoRoutablePage(IUmbracoContext context, HttpContextBase httpContext) { var uri = context.OriginalRequestUrl; @@ -186,8 +183,8 @@ namespace Umbraco.Web return Attempt.If(reason == EnsureRoutableOutcome.IsRoutable, reason); } - - + // TODO: Where should this execute in netcore? This will have to be a middleware + // executing before UseRouting so that it is done before any endpoint routing takes place. private bool EnsureRuntime(HttpContextBase httpContext, Uri uri) { var level = _runtime.Level;