Moves routing into IAreaRoutes, ensures back office is redirected if installed

This commit is contained in:
Shannon
2020-05-13 14:49:00 +10:00
parent 2e51b11f7c
commit 4d6df1405f
17 changed files with 314 additions and 154 deletions

View File

@@ -53,6 +53,8 @@
public static class Mvc
{
public const string InstallArea = "UmbracoInstall";
public const string BackOfficeArea = "UmbracoBackOffice";
}
}
}

View File

@@ -10,23 +10,22 @@ using Umbraco.Web.WebAssets;
namespace Umbraco.Web.BackOffice.Controllers
{
[Area(Umbraco.Core.Constants.Web.Mvc.BackOfficeArea)]
public class BackOfficeController : Controller
{
private readonly IRuntimeMinifier _runtimeMinifier;
private readonly IGlobalSettings _globalSettings;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime;
public BackOfficeController(IRuntimeMinifier runtimeMinifier, IGlobalSettings globalSettings, IHostingEnvironment hostingEnvironment, IUmbracoApplicationLifetime umbracoApplicationLifetime)
public BackOfficeController(IRuntimeMinifier runtimeMinifier, IGlobalSettings globalSettings, IHostingEnvironment hostingEnvironment)
{
_runtimeMinifier = runtimeMinifier;
_globalSettings = globalSettings;
_hostingEnvironment = hostingEnvironment;
_umbracoApplicationLifetime = umbracoApplicationLifetime;
}
// GET
public IActionResult Index()
[HttpGet]
public IActionResult Default()
{
return View();
}
@@ -36,6 +35,7 @@ namespace Umbraco.Web.BackOffice.Controllers
/// </summary>
/// <returns></returns>
[MinifyJavaScriptResult(Order = 0)]
[HttpGet]
public async Task<IActionResult> Application()
{
var result = await _runtimeMinifier.GetScriptForLoadingBackOfficeAsync(_globalSettings, _hostingEnvironment);

View File

@@ -1,7 +1,8 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using SixLabors.ImageSharp.Web.DependencyInjection;
using Umbraco.Extensions;
using Umbraco.Web.BackOffice.Routing;
namespace Umbraco.Extensions
{
@@ -15,18 +16,8 @@ namespace Umbraco.Extensions
app.UseEndpoints(endpoints =>
{
// TODO: This is temporary, 'umbraco' cannot be hard coded, needs to use GetUmbracoMvcArea()
// but actually we need to route all back office stuff in a back office area like we do in v8
// TODO: We will also need to detect runtime state here and redirect to the installer,
// Potentially switch this to dynamic routing so we can essentially disable/overwrite the back office routes to redirect to install
// when required, example https://www.strathweb.com/2019/08/dynamic-controller-routing-in-asp-net-core-3-0/
endpoints.MapControllerRoute("Backoffice", "/umbraco/{Action}", new
{
Controller = "BackOffice",
Action = "Default"
});
var backOfficeRoutes = app.ApplicationServices.GetRequiredService<BackOfficeAreaRoutes>();
backOfficeRoutes.CreateRoutes(endpoints);
});
app.UseUmbracoRuntimeMinification();

View File

@@ -0,0 +1,55 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Hosting;
using Umbraco.Extensions;
using Umbraco.Web.BackOffice.Controllers;
using Umbraco.Web.Common.Routing;
namespace Umbraco.Web.BackOffice.Routing
{
public class BackOfficeAreaRoutes : IAreaRoutes
{
private readonly IGlobalSettings _globalSettings;
private readonly IHostingEnvironment _hostingEnvironment;
public BackOfficeAreaRoutes(IGlobalSettings globalSettings, IHostingEnvironment hostingEnvironment)
{
_globalSettings = globalSettings;
_hostingEnvironment = hostingEnvironment;
}
public void CreateRoutes(IEndpointRouteBuilder endpoints)
{
var umbracoPath = _globalSettings.GetUmbracoMvcArea(_hostingEnvironment);
// TODO: We need to auto-route "Umbraco Api Controllers" for the back office
// TODO: We will also need to detect runtime state here and redirect to the installer,
// Potentially switch this to dynamic routing so we can essentially disable/overwrite the back office routes to redirect to install
// when required, example https://www.strathweb.com/2019/08/dynamic-controller-routing-in-asp-net-core-3-0/
endpoints.MapAreaControllerRoute(
"Umbraco_back_office", // TODO: Same name as before but we should change these so they have a convention
Constants.Web.Mvc.BackOfficeArea,
$"{umbracoPath}/{{Action}}/{{id?}}",
new { controller = ControllerExtensions.GetControllerName<BackOfficeController>(), action = "Default" },
// Limit the action/id to only allow characters - this is so this route doesn't hog all other
// routes like: /umbraco/channels/word.aspx, etc...
// (Not that we have to worry about too many of those these days, there still might be a need for these constraints).
new
{
action = @"[a-zA-Z]*",
id = @"[a-zA-Z]*"
});
endpoints.MapAreaControllerRoute(
"Umbraco_preview", // TODO: Same name as before but we should change these so they have a convention
Constants.Web.Mvc.BackOfficeArea,
$"{umbracoPath}/preview/{{Action}}/{{editor?}}",
// TODO: Change this to use ControllerExtensions.GetControllerName once the PreviewController is moved to Umbraco.Web.BackOffice.Controllers
new { controller = "Preview", action = "Index" });
}
}
}

View File

@@ -0,0 +1,14 @@
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Web.BackOffice.Routing;
namespace Umbraco.Web.BackOffice.Runtime
{
public class BackOfficeComposer : IComposer
{
public void Compose(Composition composition)
{
composition.RegisterUnique<BackOfficeAreaRoutes>();
}
}
}

View File

@@ -10,7 +10,7 @@ namespace Umbraco.Extensions
/// </summary>
/// <param name="controllerType"></param>
/// <returns></returns>
internal static string GetControllerName(Type controllerType)
public static string GetControllerName(Type controllerType)
{
if (!controllerType.Name.EndsWith("Controller"))
{
@@ -24,7 +24,7 @@ namespace Umbraco.Extensions
/// </summary>
/// <param name="controllerInstance"></param>
/// <returns></returns>
internal static string GetControllerName(this Controller controllerInstance)
public static string GetControllerName(this Controller controllerInstance)
{
return GetControllerName(controllerInstance.GetType());
}
@@ -35,7 +35,7 @@ namespace Umbraco.Extensions
/// <typeparam name="T"></typeparam>
/// <returns></returns>
/// <remarks></remarks>
internal static string GetControllerName<T>()
public static string GetControllerName<T>()
{
return GetControllerName(typeof(T));
}

View File

@@ -0,0 +1,32 @@
using System;
using Umbraco.Core;
using Microsoft.AspNetCore.Routing;
using System.Reflection;
namespace Umbraco.Extensions
{
public static class LinkGeneratorExtensions
{
/// <summary>
/// Return the back office url if the back office is installed
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public static string GetBackOfficeUrl(this LinkGenerator linkGenerator)
{
Type backOfficeControllerType;
try
{
backOfficeControllerType = Assembly.Load("Umbraco.Web.BackOffice")?.GetType("Umbraco.Web.BackOffice.Controllers.BackOfficeController");
if (backOfficeControllerType == null) return "/"; // this would indicate that the installer is installed without the back office
}
catch (Exception)
{
return "/"; // this would indicate that the installer is installed without the back office
}
return linkGenerator.GetPathByAction("Default", ControllerExtensions.GetControllerName(backOfficeControllerType), new { area = Constants.Web.Mvc.BackOfficeArea });
}
}
}

View File

@@ -1,10 +1,6 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Web;
using Umbraco.Web.Common.Install;
namespace Umbraco.Extensions
{
@@ -19,60 +15,14 @@ namespace Umbraco.Extensions
{
app.UseEndpoints(endpoints =>
{
var runtime = app.ApplicationServices.GetRequiredService<IRuntimeState>();
var logger = app.ApplicationServices.GetRequiredService<ILogger>();
var uriUtility = app.ApplicationServices.GetRequiredService<UriUtility>();
switch (runtime.Level)
{
case RuntimeLevel.Install:
case RuntimeLevel.Upgrade:
var installPath = uriUtility.ToAbsolute(Constants.SystemDirectories.Install).EnsureEndsWith('/');
endpoints.MapAreaControllerRoute(
"umbraco-install-api",
Umbraco.Core.Constants.Web.Mvc.InstallArea,
$"{installPath}api/{{Action}}",
new { controller = "InstallApi" });
endpoints.MapAreaControllerRoute(
"umbraco-install",
Umbraco.Core.Constants.Web.Mvc.InstallArea,
$"{installPath}{{controller}}/{{Action}}",
new { controller = "Install", action = "Index" });
// TODO: Potentially switch this to dynamic routing so we can essentially disable/overwrite the back office routes to redirect to install,
// example https://www.strathweb.com/2019/08/dynamic-controller-routing-in-asp-net-core-3-0/
// register catch all because if we are in install/upgrade mode then we'll catch everything and redirect
endpoints.MapGet("{*url}", context =>
{
var uri = context.Request.GetEncodedUrl();
// redirect to install
ReportRuntime(logger, runtime.Level, "Umbraco must install or upgrade.");
var installUrl = $"{installPath}?redir=true&url={uri}";
context.Response.Redirect(installUrl, false);
return Task.CompletedTask;
});
break;
}
var backOfficeRoutes = app.ApplicationServices.GetRequiredService<InstallAreaRoutes>();
backOfficeRoutes.CreateRoutes(endpoints);
});
return app;
}
private static bool _reported;
private static RuntimeLevel _reportedLevel;
private static void ReportRuntime(ILogger logger, RuntimeLevel level, string message)
{
if (_reported && _reportedLevel == level) return;
_reported = true;
_reportedLevel = level;
logger.Warn(typeof(UmbracoInstallApplicationBuilderExtensions), message);
}
}
}

View File

@@ -4,14 +4,39 @@ using System.Linq.Expressions;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Core;
using Umbraco.Web.Common.Controllers;
using Umbraco.Extensions;
using Umbraco.Web.WebApi;
using Microsoft.AspNetCore.Mvc.Routing;
using Umbraco.Web.Common.Install;
namespace Umbraco.Extensions
{
public static class HttpUrlHelperExtensions
public static class UrlHelperExtensions
{
/// <summary>
/// Return the back office url if the back office is installed
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public static string GetBackOfficeUrl(this IUrlHelper url)
{
var backOfficeControllerType = Type.GetType("Umbraco.Web.BackOffice.Controllers");
if (backOfficeControllerType == null) return "/"; // this would indicate that the installer is installed without the back office
return url.Action("Default", ControllerExtensions.GetControllerName(backOfficeControllerType), new { area = Constants.Web.Mvc.BackOfficeArea });
}
/// <summary>
/// Return the installer API url
/// </summary>
/// <param name="url"></param>
/// <returns></returns>
public static string GetInstallerApiUrl(this IUrlHelper url)
{
// there is no default action here so we need to get it by action and trim the action
return url.Action("GetSetup", ControllerExtensions.GetControllerName<InstallApiController>(), new { area = Constants.Web.Mvc.InstallArea })
.TrimEnd("GetSetup");
}
/// <summary>
/// Return the Url for a Web Api service
/// </summary>

View File

@@ -1,55 +0,0 @@
using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Core;
using Umbraco.Core.Logging;
namespace Umbraco.Web.Install
{
/// <summary>
/// Ensures authorization occurs for the installer if it has already completed.
/// If install has not yet occurred then the authorization is successful.
/// </summary>
public class HttpInstallAuthorizeAttribute : TypeFilterAttribute
{
public HttpInstallAuthorizeAttribute() : base(typeof(HttpInstallAuthorizeFilter))
{
}
private class HttpInstallAuthorizeFilter : IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext authorizationFilterContext)
{
var serviceProvider = authorizationFilterContext.HttpContext.RequestServices;
var runtimeState = serviceProvider.GetService<IRuntimeState>();
var umbracoContext = serviceProvider.GetService<IUmbracoContext>();
var logger = serviceProvider.GetService<ILogger>();
if (!IsAllowed(runtimeState, umbracoContext, logger))
{
authorizationFilterContext.Result = new ForbidResult();
}
}
private static bool IsAllowed(IRuntimeState runtimeState, IUmbracoContext umbracoContext, ILogger logger)
{
try
{
// if not configured (install or upgrade) then we can continue
// otherwise we need to ensure that a user is logged in
return runtimeState.Level == RuntimeLevel.Install
|| runtimeState.Level == RuntimeLevel.Upgrade
|| (umbracoContext?.Security?.ValidateCurrentUser() ?? false);
}
catch (Exception ex)
{
logger.Error<HttpInstallAuthorizeAttribute>(ex, "An error occurred determining authorization");
return false;
}
}
}
}
}

View File

@@ -23,7 +23,7 @@ namespace Umbraco.Web.Common.Install
[UmbracoApiController]
[TypeFilter(typeof(HttpResponseExceptionFilter))]
[TypeFilter(typeof(AngularJsonOnlyConfigurationAttribute))]
[HttpInstallAuthorize]
[Web.Install.InstallAuthorize]
[Area(Umbraco.Core.Constants.Web.Mvc.InstallArea)]
public class InstallApiController : ControllerBase
{

View File

@@ -0,0 +1,93 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Routing;
using System.Threading.Tasks;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Extensions;
using Umbraco.Web.Common.Routing;
namespace Umbraco.Web.Common.Install
{
public class InstallAreaRoutes : IAreaRoutes
{
private readonly IRuntimeState _runtime;
private readonly ILogger _logger;
private readonly UriUtility _uriUtility;
private readonly LinkGenerator _linkGenerator;
public InstallAreaRoutes(IRuntimeState runtime, ILogger logger, UriUtility uriUtility, LinkGenerator linkGenerator)
{
_runtime = runtime;
_logger = logger;
_uriUtility = uriUtility;
_linkGenerator = linkGenerator;
}
public void CreateRoutes(IEndpointRouteBuilder endpoints)
{
var installPath = _uriUtility.ToAbsolute(Umbraco.Core.Constants.SystemDirectories.Install).EnsureEndsWith('/');
switch (_runtime.Level)
{
case RuntimeLevel.Install:
case RuntimeLevel.Upgrade:
endpoints.MapAreaControllerRoute(
"umbraco-install-api",
Umbraco.Core.Constants.Web.Mvc.InstallArea,
$"{installPath}api/{{Action}}",
new { controller = ControllerExtensions.GetControllerName<InstallApiController>() });
endpoints.MapAreaControllerRoute(
"umbraco-install",
Umbraco.Core.Constants.Web.Mvc.InstallArea,
$"{installPath}{{controller}}/{{Action}}",
new { controller = ControllerExtensions.GetControllerName<InstallController>(), action = "Index" });
// TODO: Potentially switch this to dynamic routing so we can essentially disable/overwrite the back office routes to redirect to install,
// example https://www.strathweb.com/2019/08/dynamic-controller-routing-in-asp-net-core-3-0/
// register catch all because if we are in install/upgrade mode then we'll catch everything and redirect
endpoints.MapGet("{*url}", context =>
{
var uri = context.Request.GetEncodedUrl();
// redirect to install
ReportRuntime(_logger, _runtime.Level, "Umbraco must install or upgrade.");
var installUrl = $"{installPath}?redir=true&url={uri}";
context.Response.Redirect(installUrl, false);
return Task.CompletedTask;
});
break;
case RuntimeLevel.Run:
// when we are in run mode redirect to the back office if the installer endpoint is hit
endpoints.MapGet($"{installPath}{{controller?}}/{{Action?}}", context =>
{
// redirect to umbraco
context.Response.Redirect(_linkGenerator.GetBackOfficeUrl(), false);
return Task.CompletedTask;
});
break;
case RuntimeLevel.BootFailed:
case RuntimeLevel.Unknown:
case RuntimeLevel.Boot:
break;
}
}
private static bool _reported;
private static RuntimeLevel _reportedLevel;
private static void ReportRuntime(ILogger logger, RuntimeLevel level, string message)
{
if (_reported && _reportedLevel == level) return;
_reported = true;
_reportedLevel = level;
logger.Warn(typeof(UmbracoInstallApplicationBuilderExtensions), message);
}
}
}

View File

@@ -3,34 +3,37 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Hosting;
using Umbraco.Core.Logging;
namespace Umbraco.Web.Common.Install
namespace Umbraco.Web.Install
{
/// <summary>
/// Ensures authorization occurs for the installer if it has already completed.
/// If install has not yet occurred then the authorization is successful.
/// </summary>
public class InstallAuthorizeAttribute : TypeFilterAttribute
{
public InstallAuthorizeAttribute() : base(typeof(InstallAuthorizeFilter))
public InstallAuthorizeAttribute() : base(typeof(HttpInstallAuthorizeFilter))
{
}
private class InstallAuthorizeFilter : IAuthorizationFilter
private class HttpInstallAuthorizeFilter : IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
public void OnAuthorization(AuthorizationFilterContext authorizationFilterContext)
{
var sp = context.HttpContext.RequestServices;
var runtimeState = sp.GetRequiredService<IRuntimeState>();
var umbracoContextAccessor = sp.GetRequiredService<IUmbracoContextAccessor>();
var globalSettings = sp.GetRequiredService<IGlobalSettings>();
var hostingEnvironment = sp.GetRequiredService<IHostingEnvironment>();
var serviceProvider = authorizationFilterContext.HttpContext.RequestServices;
var runtimeState = serviceProvider.GetService<IRuntimeState>();
var umbracoContext = serviceProvider.GetService<IUmbracoContext>();
var logger = serviceProvider.GetService<ILogger>();
if (!IsAllowed(runtimeState, umbracoContextAccessor))
if (!IsAllowed(runtimeState, umbracoContext, logger))
{
context.Result = new RedirectResult(globalSettings.GetBackOfficePath(hostingEnvironment));
authorizationFilterContext.Result = new ForbidResult();
}
}
private bool IsAllowed(IRuntimeState runtimeState, IUmbracoContextAccessor umbracoContextAccessor)
private static bool IsAllowed(IRuntimeState runtimeState, IUmbracoContext umbracoContext, ILogger logger)
{
try
{
@@ -38,13 +41,15 @@ namespace Umbraco.Web.Common.Install
// otherwise we need to ensure that a user is logged in
return runtimeState.Level == RuntimeLevel.Install
|| runtimeState.Level == RuntimeLevel.Upgrade
|| umbracoContextAccessor.UmbracoContext.Security.ValidateCurrentUser();
|| (umbracoContext?.Security?.ValidateCurrentUser() ?? false);
}
catch (Exception)
catch (Exception ex)
{
logger.Error<InstallAuthorizeAttribute>(ex, "An error occurred determining authorization");
return false;
}
}
}
}
}

View File

@@ -51,8 +51,10 @@ namespace Umbraco.Web.Common.Install
[TypeFilter(typeof(StatusCodeResultAttribute), Arguments = new object []{System.Net.HttpStatusCode.ServiceUnavailable})]
public async Task<ActionResult> Index()
{
var umbracoPath = Url.GetBackOfficeUrl();
if (_runtime.Level == RuntimeLevel.Run)
return Redirect(_globalSettings.UmbracoPath.EnsureEndsWith('/'));
return Redirect(umbracoPath);
if (_runtime.Level == RuntimeLevel.Upgrade)
{
@@ -69,8 +71,8 @@ namespace Umbraco.Web.Common.Install
}
}
// gen the install base urlAddUmbracoCore
ViewData.SetInstallApiBaseUrl(Url.GetUmbracoApiService("GetSetup", "InstallApi", Umbraco.Core.Constants.Web.Mvc.InstallArea).TrimEnd("GetSetup"));
// gen the install base url
ViewData.SetInstallApiBaseUrl(Url.GetInstallerApiUrl());
// get the base umbraco folder
ViewData.SetUmbracoBaseFolder(_hostingEnvironment.ToAbsolute(_globalSettings.UmbracoPath));

View File

@@ -0,0 +1,13 @@
using Umbraco.Core;
using Umbraco.Core.Composing;
namespace Umbraco.Web.Common.Install
{
public class InstallerComposer : IComposer
{
public void Compose(Composition composition)
{
composition.RegisterUnique<InstallAreaRoutes>();
}
}
}

View File

@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Routing;
namespace Umbraco.Web.Common.Routing
{
/// <summary>
/// Used to create routes for a route area
/// </summary>
public interface IAreaRoutes
{
// TODO: It could be possible to just get all collections of IAreaRoutes and route them all instead of relying
// 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.
void CreateRoutes(IEndpointRouteBuilder endpoints);
}
}

View File

@@ -0,0 +1,17 @@
@{
Layout = null;
}
<!doctype html>
<html lang="en">
<head>
</head>
<body>
<h1>Hello!</h1>
<p>TODO: Import the default.cshtml back office page</p>
</body>
</html>