Merge remote-tracking branch 'origin/v10/dev' into v11/dev

# Conflicts:
#	src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html
#	tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/InstallAreaRoutesTests.cs
This commit is contained in:
Bjarke Berg
2022-12-14 08:51:45 +01:00
15 changed files with 233 additions and 36 deletions

View File

@@ -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 &rarr;</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 &rarr;</a>
</div>
</div>
</div>
</article>
</section>
</body>
</html>

View File

@@ -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;
}

View File

@@ -68,6 +68,9 @@ public class MultipleTextStringPropertyEditor : DataEditor
/// </summary>
internal class MultipleTextStringPropertyValueEditor : DataValueEditor
{
private static readonly string NewLine = "\n";
private static readonly string[] NewLineDelimiters = { "\r\n", "\r", "\n" };
private readonly ILocalizedTextService _localizedTextService;
public MultipleTextStringPropertyValueEditor(
@@ -119,10 +122,10 @@ public class MultipleTextStringPropertyEditor : DataEditor
// only allow the max if over 0
if (max > 0)
{
return string.Join(Environment.NewLine, array.Take(max));
return string.Join(NewLine, array.Take(max));
}
return string.Join(Environment.NewLine, array);
return string.Join(NewLine, array);
}
/// <summary>
@@ -131,7 +134,6 @@ public class MultipleTextStringPropertyEditor : DataEditor
/// cannot have 2 way binding, so to get around that each item in the array needs to be an object with a string.
/// </summary>
/// <param name="property"></param>
/// <param name="dataTypeService"></param>
/// <param name="culture"></param>
/// <param name="segment"></param>
/// <returns></returns>
@@ -141,8 +143,9 @@ public class MultipleTextStringPropertyEditor : DataEditor
public override object ToEditor(IProperty property, string? culture = null, string? segment = null)
{
var val = property.GetValue(culture, segment);
return val?.ToString()?.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => JObject.FromObject(new { value = x })) ?? new JObject[] { };
return val?.ToString()?.Split(NewLineDelimiters, StringSplitOptions.None).Select(x => JObject.FromObject(new { value = x }))
?? Array.Empty<JObject>();
}
}

View File

@@ -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("/");
}

View File

@@ -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:

View 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);
}
}

View File

@@ -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)

View File

@@ -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)
{
}
}
}

View File

@@ -16,6 +16,7 @@ namespace Umbraco.Cms.Web.Common.Controllers;
/// </summary>
[ModelBindingException]
[PublishedRequestFilter]
[MaintenanceModeActionFilter]
public class RenderController : UmbracoPageController, IRenderController
{
private readonly ILogger<RenderController> _logger;

View File

@@ -181,7 +181,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, () =>

View File

@@ -22,6 +22,9 @@
}
.umb-block-grid__area {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
--umb-block-grid__area-calc: calc(var(--umb-block-grid--area-column-span) / var(--umb-block-grid--area-grid-columns, 1));
width: calc(var(--umb-block-grid__area-calc) * 100% - (1 - var(--umb-block-grid__area-calc)) * var(--umb-block-grid--areas-column-gap, 0px));
}

View File

@@ -28,8 +28,11 @@
}
.umb-block-grid__area {
position: relative;
height: 100%;
display: flex;
flex-direction: column;
/* For small devices we scale columnSpan by three, to make everything bigger than 1/3 take full width: */
grid-column-end: span min(calc(var(--umb-block-grid--area-column-span, 1) * 3), var(--umb-block-grid--grid-columns));
grid-column-end: span min(calc(var(--umb-block-grid--area-column-span, 1) * 3), var(--umb-block-grid--area-grid-columns));
grid-row: span var(--umb-block-grid--area-row-span, 1);
}
@@ -38,6 +41,6 @@
grid-column-end: span min(var(--umb-block-grid--item-column-span, 1), var(--umb-block-grid--grid-columns));
}
.umb-block-grid__area {
grid-column-end: span var(--umb-block-grid--area-column-span, 1);
grid-column-end: span min(var(--umb-block-grid--area-column-span, 1), var(--umb-block-grid--area-grid-columns));
}
}

View File

@@ -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>

View File

@@ -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!;
}

View File

@@ -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 endpoint = (RouteEndpoint)route.Endpoints[0];
Assert.AreEqual("install/{controller?}/{action?}", endpoint.RoutePattern.RawText);
Assert.AreEqual("install/api/{action}/{id?}", routeEndpoint.RoutePattern.RawText);
routeEndpoint = (RouteEndpoint)route.Endpoints[1];
Assert.AreEqual("install/{action}/{id?}", endpoint.RoutePattern.RawText);
}
private InstallAreaRoutes GetInstallAreaRoutes(RuntimeLevel level) =>