Maintenance page when in upgrade state (#13551)
* Added functionality to show maintenance page and fixed issues with showing custom api controllers and 404 page, when umbraco had been in install or upgrade state * Fixed Tests * Fixed typo * Fixed issue with login screen redirecting to website when in upgrade state, instead of backoffice
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
@using Microsoft.Extensions.Options
|
||||
@using Umbraco.Cms.Core.Configuration.Models
|
||||
@using Umbraco.Cms.Core.Hosting
|
||||
@using Umbraco.Cms.Core.Routing
|
||||
@using Umbraco.Extensions
|
||||
@inject IHostingEnvironment hostingEnvironment
|
||||
@inject IOptions<GlobalSettings> globalSettings
|
||||
@{
|
||||
var backOfficePath = globalSettings.Value.GetBackOfficePath(hostingEnvironment);
|
||||
}
|
||||
<!doctype html>
|
||||
<html class="no-js" lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
|
||||
<title>Website is Under Maintainance</title>
|
||||
|
||||
<link rel="stylesheet" href="@WebPath.Combine(backOfficePath.TrimStart("~"), "/assets/css/nonodes.style.min.css")" />
|
||||
<style type="text/css">
|
||||
body {
|
||||
color:initial;
|
||||
}
|
||||
|
||||
section {
|
||||
background: none;
|
||||
}
|
||||
|
||||
section a, section a:focus, section a:visited {
|
||||
color:initial;
|
||||
border-color:currentColor;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<section>
|
||||
<article>
|
||||
<div>
|
||||
<h1>Website is Under Maintenance</h1>
|
||||
|
||||
<div class="cta"></div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h2>This page can be replaced</h2>
|
||||
<p>
|
||||
Custom error handling might make your site look more on-brand and minimize the impact of errors on user experience - for example, a custom 404 with some helpful links (or a search function) could bring some value to the site.
|
||||
</p>
|
||||
|
||||
<a href="https://umbra.co/custom-error-pages" target="_blank" rel="noopener">Implementing custom error pages →</a>
|
||||
</div>
|
||||
|
||||
<div class="col">
|
||||
<h2>Finish the maintenance</h2>
|
||||
<p>If you are an administrator, you finish the maintanance by going to backoffice.</p>
|
||||
|
||||
<a href="@backOfficePath">Handle the upgrade →</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</article>
|
||||
|
||||
</section>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -33,6 +33,7 @@ public class GlobalSettings
|
||||
internal const bool StaticSanitizeTinyMce = false;
|
||||
internal const int StaticMainDomReleaseSignalPollingInterval = 2000;
|
||||
private const bool StaticForceCombineUrlPathLeftToRight = true;
|
||||
private const bool StaticShowMaintenancePageWhenInUpgradeState = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value for the reserved URLs (must end with a comma).
|
||||
@@ -252,4 +253,7 @@ public class GlobalSettings
|
||||
/// </example>
|
||||
[DefaultValue(StaticForceCombineUrlPathLeftToRight)]
|
||||
public bool ForceCombineUrlPathLeftToRight { get; set; } = StaticForceCombineUrlPathLeftToRight;
|
||||
|
||||
[DefaultValue(StaticShowMaintenancePageWhenInUpgradeState)]
|
||||
public bool ShowMaintenancePageWhenInUpgradeState { get; set; } = StaticShowMaintenancePageWhenInUpgradeState;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
@@ -120,6 +121,13 @@ public class BackOfficeController : UmbracoController
|
||||
// Check if we not are in an run state, if so we need to redirect
|
||||
if (_runtimeState.Level != RuntimeLevel.Run)
|
||||
{
|
||||
if (_runtimeState.Level == RuntimeLevel.Upgrade)
|
||||
{
|
||||
return RedirectToAction(nameof(AuthorizeUpgrade), routeValues: new RouteValueDictionary()
|
||||
{
|
||||
["redir"] = _globalSettings.GetBackOfficePath(_hostingEnvironment),
|
||||
});
|
||||
}
|
||||
return Redirect("/");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Hosting;
|
||||
@@ -28,23 +29,12 @@ public class InstallAreaRoutes : IAreaRoutes
|
||||
|
||||
switch (_runtime.Level)
|
||||
{
|
||||
case var _ when _runtime.EnableInstaller():
|
||||
case RuntimeLevel.Install:
|
||||
case RuntimeLevel.Upgrade:
|
||||
case RuntimeLevel.Run:
|
||||
|
||||
endpoints.MapUmbracoRoute<InstallApiController>(installPathSegment, Constants.Web.Mvc.InstallArea, "api", includeControllerNameInRoute: false);
|
||||
endpoints.MapUmbracoRoute<InstallController>(installPathSegment, Constants.Web.Mvc.InstallArea, string.Empty, includeControllerNameInRoute: false);
|
||||
|
||||
// register catch all because if we are in install/upgrade mode then we'll catch everything
|
||||
endpoints.MapFallbackToAreaController(nameof(InstallController.Index), ControllerExtensions.GetControllerName<InstallController>(), Constants.Web.Mvc.InstallArea);
|
||||
|
||||
break;
|
||||
case RuntimeLevel.Run:
|
||||
// when we are in run mode redirect to the back office if the installer endpoint is hit
|
||||
endpoints.MapGet($"{installPathSegment}/{{controller?}}/{{action?}}", context =>
|
||||
{
|
||||
// redirect to umbraco
|
||||
context.Response.Redirect(_linkGenerator.GetBackOfficeUrl(_hostingEnvironment)!, false);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
|
||||
break;
|
||||
case RuntimeLevel.BootFailed:
|
||||
case RuntimeLevel.Unknown:
|
||||
|
||||
27
src/Umbraco.Web.Common/ActionsResults/MaintenanceResult.cs
Normal file
27
src/Umbraco.Web.Common/ActionsResults/MaintenanceResult.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Core.Routing;
|
||||
using Umbraco.Cms.Core.Web;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.ActionsResults;
|
||||
|
||||
/// <summary>
|
||||
/// Returns the Umbraco maintenance result
|
||||
/// </summary>
|
||||
public class MaintenanceResult : IActionResult
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task ExecuteResultAsync(ActionContext context)
|
||||
{
|
||||
HttpResponse response = context.HttpContext.Response;
|
||||
|
||||
response.Clear();
|
||||
|
||||
response.StatusCode = StatusCodes.Status503ServiceUnavailable;
|
||||
|
||||
var viewResult = new ViewResult { ViewName = "~/umbraco/UmbracoWebsite/Maintenance.cshtml" };
|
||||
|
||||
await viewResult.ExecuteResultAsync(context);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,11 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http.Features;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Features;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.DependencyInjection;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.Authorization;
|
||||
|
||||
@@ -13,8 +17,20 @@ namespace Umbraco.Cms.Web.Common.Authorization;
|
||||
public class FeatureAuthorizeHandler : AuthorizationHandler<FeatureAuthorizeRequirement>
|
||||
{
|
||||
private readonly UmbracoFeatures _umbracoFeatures;
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
|
||||
public FeatureAuthorizeHandler(UmbracoFeatures umbracoFeatures) => _umbracoFeatures = umbracoFeatures;
|
||||
public FeatureAuthorizeHandler(UmbracoFeatures umbracoFeatures, IRuntimeState runtimeState)
|
||||
{
|
||||
_umbracoFeatures = umbracoFeatures;
|
||||
_runtimeState = runtimeState;
|
||||
}
|
||||
|
||||
[Obsolete("Use ctor that is not obsolete. This will be removed in v13.")]
|
||||
public FeatureAuthorizeHandler(UmbracoFeatures umbracoFeatures)
|
||||
:this(umbracoFeatures, StaticServiceProvider.Instance.GetRequiredService<IRuntimeState>())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override Task HandleRequirementAsync(
|
||||
AuthorizationHandlerContext context,
|
||||
@@ -35,6 +51,11 @@ public class FeatureAuthorizeHandler : AuthorizationHandler<FeatureAuthorizeRequ
|
||||
|
||||
private bool? IsAllowed(AuthorizationHandlerContext context)
|
||||
{
|
||||
if(_runtimeState.Level != RuntimeLevel.Run && _runtimeState.Level != RuntimeLevel.Upgrade)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Endpoint? endpoint = null;
|
||||
|
||||
switch (context.Resource)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Web.Common.ActionsResults;
|
||||
|
||||
namespace Umbraco.Cms.Web.Common.Controllers;
|
||||
|
||||
internal sealed class MaintenanceModeActionFilterAttribute : TypeFilterAttribute
|
||||
{
|
||||
public MaintenanceModeActionFilterAttribute() : base(typeof(MaintenanceModeActionFilter))
|
||||
{
|
||||
}
|
||||
|
||||
private sealed class MaintenanceModeActionFilter : IActionFilter
|
||||
{
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
private readonly IOptionsMonitor<GlobalSettings> _globalSettings;
|
||||
|
||||
public MaintenanceModeActionFilter(IRuntimeState runtimeState, IOptionsMonitor<GlobalSettings> globalSettings)
|
||||
{
|
||||
_runtimeState = runtimeState;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public void OnActionExecuting(ActionExecutingContext context)
|
||||
{
|
||||
if (_runtimeState.Level == RuntimeLevel.Upgrade && _globalSettings.CurrentValue.ShowMaintenancePageWhenInUpgradeState)
|
||||
{
|
||||
context.Result = new MaintenanceResult();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void OnActionExecuted(ActionExecutedContext context)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ namespace Umbraco.Cms.Web.Common.Controllers;
|
||||
/// </summary>
|
||||
[ModelBindingException]
|
||||
[PublishedRequestFilter]
|
||||
[MaintenanceModeActionFilter]
|
||||
public class RenderController : UmbracoPageController, IRenderController
|
||||
{
|
||||
private readonly ILogger<RenderController> _logger;
|
||||
|
||||
@@ -216,7 +216,8 @@ public class UmbracoRequestMiddleware : IMiddleware
|
||||
|
||||
if (_umbracoRequestPaths.IsBackOfficeRequest(absPath)
|
||||
|| (absPath.Value?.InvariantStartsWith($"/{_smidgeOptions.UrlOptions.CompositeFilePath}") ?? false)
|
||||
|| (absPath.Value?.InvariantStartsWith($"/{_smidgeOptions.UrlOptions.BundleFilePath}") ?? false))
|
||||
|| (absPath.Value?.InvariantStartsWith($"/{_smidgeOptions.UrlOptions.BundleFilePath}") ?? false)
|
||||
|| _runtimeState.EnableInstaller())
|
||||
{
|
||||
LazyInitializer.EnsureInitialized(ref s_firstBackOfficeRequest, ref s_firstBackOfficeReqestFlag,
|
||||
ref s_firstBackOfficeRequestLocker, () =>
|
||||
|
||||
@@ -41,13 +41,22 @@ public sealed class FrontEndRoutes : IAreaRoutes
|
||||
/// <inheritdoc />
|
||||
public void CreateRoutes(IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
if (_runtimeState.Level != RuntimeLevel.Run)
|
||||
switch (_runtimeState.Level)
|
||||
{
|
||||
return;
|
||||
case RuntimeLevel.Install:
|
||||
case RuntimeLevel.Upgrade:
|
||||
case RuntimeLevel.Run:
|
||||
|
||||
AutoRouteSurfaceControllers(endpoints);
|
||||
AutoRouteFrontEndApiControllers(endpoints);
|
||||
break;
|
||||
case RuntimeLevel.BootFailed:
|
||||
case RuntimeLevel.Unknown:
|
||||
case RuntimeLevel.Boot:
|
||||
break;
|
||||
}
|
||||
|
||||
AutoRouteSurfaceControllers(endpoints);
|
||||
AutoRouteFrontEndApiControllers(endpoints);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -117,9 +117,20 @@ public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer
|
||||
public override async ValueTask<RouteValueDictionary> TransformAsync(
|
||||
HttpContext httpContext, RouteValueDictionary values)
|
||||
{
|
||||
// If we aren't running, then we have nothing to route
|
||||
if (_runtime.Level != RuntimeLevel.Run)
|
||||
// If we aren't running, then we have nothing to route. We allow the frontend to continue while in upgrade mode.
|
||||
if (_runtime.Level != RuntimeLevel.Run && _runtime.Level != RuntimeLevel.Upgrade)
|
||||
{
|
||||
if (_runtime.Level == RuntimeLevel.Install)
|
||||
{
|
||||
return new RouteValueDictionary()
|
||||
{
|
||||
//TODO figure out constants
|
||||
[ControllerToken] = "Install",
|
||||
[ActionToken] = "Index",
|
||||
[AreaToken] = Constants.Web.Mvc.InstallArea,
|
||||
};
|
||||
}
|
||||
|
||||
return null!;
|
||||
}
|
||||
|
||||
@@ -184,6 +195,7 @@ public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer
|
||||
// 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!;
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ public class InstallAreaRoutesTests
|
||||
var endpoints = new TestRouteBuilder();
|
||||
routes.CreateRoutes(endpoints);
|
||||
|
||||
Assert.AreEqual(2, endpoints.DataSources.Count);
|
||||
Assert.AreEqual(1, endpoints.DataSources.Count);
|
||||
var route = endpoints.DataSources.First();
|
||||
Assert.AreEqual(2, route.Endpoints.Count);
|
||||
|
||||
@@ -64,10 +64,11 @@ public class InstallAreaRoutesTests
|
||||
endpoint2.RoutePattern.Defaults[AreaToken],
|
||||
typeof(InstallController).GetCustomAttribute<AreaAttribute>(false).RouteValue);
|
||||
|
||||
var fallbackRoute = endpoints.DataSources.Last();
|
||||
Assert.AreEqual(1, fallbackRoute.Endpoints.Count);
|
||||
var dataSource = endpoints.DataSources.Last();
|
||||
Assert.AreEqual(2, dataSource.Endpoints.Count);
|
||||
|
||||
Assert.AreEqual("Fallback {*path:nonfile}", fallbackRoute.Endpoints[0].ToString());
|
||||
Assert.AreEqual("Route: install/api/{action}/{id?}", dataSource.Endpoints[0].ToString());
|
||||
Assert.AreEqual("Route: install/{action}/{id?}", dataSource.Endpoints[1].ToString());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -79,10 +80,13 @@ public class InstallAreaRoutesTests
|
||||
|
||||
Assert.AreEqual(1, endpoints.DataSources.Count);
|
||||
var route = endpoints.DataSources.First();
|
||||
Assert.AreEqual(1, route.Endpoints.Count);
|
||||
Assert.AreEqual(2, route.Endpoints.Count);
|
||||
|
||||
var routeEndpoint = (RouteEndpoint)route.Endpoints[0];
|
||||
Assert.AreEqual("install/{controller?}/{action?}", routeEndpoint.RoutePattern.RawText);
|
||||
Assert.AreEqual("install/api/{action}/{id?}", routeEndpoint.RoutePattern.RawText);
|
||||
|
||||
routeEndpoint = (RouteEndpoint)route.Endpoints[1];
|
||||
Assert.AreEqual("install/{action}/{id?}", routeEndpoint.RoutePattern.RawText);
|
||||
}
|
||||
|
||||
private InstallAreaRoutes GetInstallAreaRoutes(RuntimeLevel level) =>
|
||||
|
||||
Reference in New Issue
Block a user