From 34bd37a5b93cb79ab0cf31b4682833e3d100b081 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 10 May 2024 12:23:01 +0200 Subject: [PATCH 1/5] Revert "v14: Remove mentions of UmbracoApiController (#15863)" This reverts commit 30e2dea57a1034d79ee418377c2f5b5a86746d66. --- .../Routing/BackOfficeAreaRoutes.cs | 35 ++- .../UmbracoApiControllerTypeCollection.cs | 11 + .../UmbracoApiControllerAttribute.cs | 11 + .../Controllers/UmbracoApiController.cs | 16 ++ .../Controllers/UmbracoApiControllerBase.cs | 26 ++ ...bracoApiControllerTypeCollectionBuilder.cs | 11 + .../UmbracoBuilderExtensions.cs | 4 + .../Extensions/LinkGeneratorExtensions.cs | 79 ++++++ .../Extensions/TypeLoaderExtensions.cs | 13 + .../Extensions/UrlHelperExtensions.cs | 230 ++++++++++++++++++ .../Security/ConfigureMemberCookieOptions.cs | 12 +- .../Extensions/TypeLoaderExtensions.cs | 6 + .../Routing/FrontEndRoutes.cs | 30 ++- .../UmbracoTestServerTestBase.cs | 12 + .../Security/MemberAuthorizeTests.cs | 36 +++ .../Customizations/UmbracoCustomizations.cs | 8 + .../Routing/BackOfficeAreaRoutesTests.cs | 89 +++++++ 17 files changed, 626 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs create mode 100644 src/Umbraco.Web.Common/Attributes/UmbracoApiControllerAttribute.cs create mode 100644 src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs create mode 100644 src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs create mode 100644 src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs create mode 100644 src/Umbraco.Web.Common/Extensions/TypeLoaderExtensions.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/BackOfficeAreaRoutesTests.cs diff --git a/src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs b/src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs index 9cd03597426d..69b2dd785b3e 100644 --- a/src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs +++ b/src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs @@ -19,6 +19,7 @@ namespace Umbraco.Cms.Api.Management.Routing; /// public sealed class BackOfficeAreaRoutes : IAreaRoutes { + private readonly UmbracoApiControllerTypeCollection _apiControllers; private readonly GlobalSettings _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; private readonly IRuntimeState _runtimeState; @@ -30,11 +31,13 @@ public sealed class BackOfficeAreaRoutes : IAreaRoutes public BackOfficeAreaRoutes( IOptions globalSettings, IHostingEnvironment hostingEnvironment, - IRuntimeState runtimeState) + IRuntimeState runtimeState, + UmbracoApiControllerTypeCollection apiControllers) { _globalSettings = globalSettings.Value; _hostingEnvironment = hostingEnvironment; _runtimeState = runtimeState; + _apiControllers = apiControllers; _umbracoPathSegment = _globalSettings.GetUmbracoMvcArea(_hostingEnvironment); } @@ -50,6 +53,7 @@ public void CreateRoutes(IEndpointRouteBuilder endpoints) case RuntimeLevel.Run: MapMinimalBackOffice(endpoints); + AutoRouteBackOfficeApiControllers(endpoints); break; case RuntimeLevel.BootFailed: case RuntimeLevel.Unknown: @@ -84,4 +88,33 @@ private void MapMinimalBackOffice(IEndpointRouteBuilder endpoints) }, constraints: new { slug = @"^(section.*|upgrade|install|oauth_complete|logout|error)$" }); } + + /// + /// Auto-routes all back office api controllers + /// + private void AutoRouteBackOfficeApiControllers(IEndpointRouteBuilder endpoints) + { + // TODO: We could investigate dynamically routing plugin controllers so we don't have to eagerly type scan for them, + // it would probably work well, see https://www.strathweb.com/2019/08/dynamic-controller-routing-in-asp-net-core-3-0/ + // will probably be what we use for front-end routing too. BTW the orig article about migrating from IRouter to endpoint + // routing for things like a CMS is here https://github.com/dotnet/aspnetcore/issues/4221 + + foreach (Type controller in _apiControllers) + { + PluginControllerMetadata meta = PluginController.GetMetadata(controller); + + // exclude front-end api controllers + if (!meta.IsBackOffice) + { + continue; + } + + endpoints.MapUmbracoApiRoute( + meta.ControllerType, + _umbracoPathSegment, + meta.AreaName, + meta.IsBackOffice, + string.Empty); // no default action (this is what we had before) + } + } } diff --git a/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs b/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs new file mode 100644 index 000000000000..afd6183b548a --- /dev/null +++ b/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Core; + +public class UmbracoApiControllerTypeCollection : BuilderCollectionBase +{ + public UmbracoApiControllerTypeCollection(Func> items) + : base(items) + { + } +} diff --git a/src/Umbraco.Web.Common/Attributes/UmbracoApiControllerAttribute.cs b/src/Umbraco.Web.Common/Attributes/UmbracoApiControllerAttribute.cs new file mode 100644 index 000000000000..48d3f3404ecc --- /dev/null +++ b/src/Umbraco.Web.Common/Attributes/UmbracoApiControllerAttribute.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Web.Common.ApplicationModels; + +namespace Umbraco.Cms.Web.Common.Attributes; + +/// +/// When present on a controller then conventions will apply +/// +[AttributeUsage(AttributeTargets.Class)] +public sealed class UmbracoApiControllerAttribute : Attribute +{ +} diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs new file mode 100644 index 000000000000..05d7004e1ffe --- /dev/null +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs @@ -0,0 +1,16 @@ +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Web.Common.Controllers; + +/// +/// Provides a base class for auto-routed Umbraco API controllers. +/// +public abstract class UmbracoApiController : UmbracoApiControllerBase, IDiscoverable +{ + /// + /// Initializes a new instance of the class. + /// + protected UmbracoApiController() + { + } +} diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs new file mode 100644 index 000000000000..e5cbe66cf6cb --- /dev/null +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Features; +using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Cms.Web.Common.Authorization; + +namespace Umbraco.Cms.Web.Common.Controllers; + +/// +/// Provides a base class for Umbraco API controllers. +/// +/// +/// These controllers are NOT auto-routed. +/// The base class is which are netcore API controllers without any view support +/// +[Authorize(Policy = AuthorizationPolicies.UmbracoFeatureEnabled)] // TODO: This could be part of our conventions +[UmbracoApiController] +public abstract class UmbracoApiControllerBase : ControllerBase, IUmbracoFeature +{ + /// + /// Initializes a new instance of the class. + /// + protected UmbracoApiControllerBase() + { + } +} diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs new file mode 100644 index 000000000000..87a8c8e56dfc --- /dev/null +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Composing; + +namespace Umbraco.Cms.Web.Common.Controllers; + +public class UmbracoApiControllerTypeCollectionBuilder : TypeCollectionBuilderBase< + UmbracoApiControllerTypeCollectionBuilder, UmbracoApiControllerTypeCollection, UmbracoApiController> +{ + // TODO: Should this only exist in the back office project? These really are only ever used for the back office AFAIK + protected override UmbracoApiControllerTypeCollectionBuilder This => this; +} diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 685adf2fbdea..83b209d012f7 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -287,6 +287,10 @@ public static IUmbracoBuilder AddWebComponents(this IUmbracoBuilder builder) builder.Services.AddUnique(); builder.Services.AddUnique(); + var umbracoApiControllerTypes = builder.TypeLoader.GetUmbracoApiControllers().ToList(); + builder.WithCollectionBuilder() + .Add(umbracoApiControllerTypes); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs index 8bf96bb69f36..1c589ebe2a99 100644 --- a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs @@ -12,6 +12,64 @@ namespace Umbraco.Extensions; public static class LinkGeneratorExtensions { + /// + /// Gets the Umbraco backoffice URL (if Umbraco is installed). + /// + /// The link generator. + /// + /// The Umbraco backoffice URL. + /// + public static string? GetUmbracoBackOfficeUrl(this LinkGenerator linkGenerator) + => linkGenerator.GetPathByAction("Default", "BackOffice", new { area = Constants.Web.Mvc.BackOfficeArea }); + + /// + /// Gets the Umbraco backoffice URL (if Umbraco is installed) or application virtual path (in most cases just "/"). + /// + /// The link generator. + /// The hosting environment. + /// + /// The Umbraco backoffice URL. + /// + public static string GetUmbracoBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment) + => GetUmbracoBackOfficeUrl(linkGenerator) ?? hostingEnvironment.ApplicationVirtualPath; + + /// + /// Return the back office url if the back office is installed + /// + /// + /// This method contained a bug that would result in always returning "/". + /// + [Obsolete("Use the GetUmbracoBackOfficeUrl extension method instead. This method will be removed in Umbraco 13.")] + public static string? GetBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment) + => "/"; + + /// + /// Return the Url for a Web Api service + /// + /// The + public static string? GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, object? id = null) + where T : UmbracoApiControllerBase => linkGenerator.GetUmbracoControllerUrl( + actionName, + typeof(T), + new Dictionary { ["id"] = id }); + + public static string? GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, IDictionary? values) + where T : UmbracoApiControllerBase => linkGenerator.GetUmbracoControllerUrl(actionName, typeof(T), values); + + public static string? GetUmbracoApiServiceBaseUrl( + this LinkGenerator linkGenerator, + Expression> methodSelector) + where T : UmbracoApiControllerBase + { + MethodInfo? method = ExpressionHelper.GetMethodInfo(methodSelector); + if (method == null) + { + throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + + " or the result "); + } + + return linkGenerator.GetUmbracoApiService(method.Name)?.TrimEnd(method.Name); + } /// /// Return the Url for an Umbraco controller @@ -101,4 +159,25 @@ public static class LinkGeneratorExtensions return linkGenerator.GetUmbracoControllerUrl(actionName, ControllerExtensions.GetControllerName(controllerType), area, values); } + + public static string? GetUmbracoApiService( + this LinkGenerator linkGenerator, + Expression> methodSelector) + where T : UmbracoApiController + { + MethodInfo? method = ExpressionHelper.GetMethodInfo(methodSelector); + IDictionary? methodParams = ExpressionHelper.GetMethodParams(methodSelector); + if (method == null) + { + throw new MissingMethodException( + $"Could not find the method {methodSelector} on type {typeof(T)} or the result "); + } + + if (methodParams?.Any() == false) + { + return linkGenerator.GetUmbracoApiService(method.Name); + } + + return linkGenerator.GetUmbracoApiService(method.Name, methodParams); + } } diff --git a/src/Umbraco.Web.Common/Extensions/TypeLoaderExtensions.cs b/src/Umbraco.Web.Common/Extensions/TypeLoaderExtensions.cs new file mode 100644 index 000000000000..423ea52536c9 --- /dev/null +++ b/src/Umbraco.Web.Common/Extensions/TypeLoaderExtensions.cs @@ -0,0 +1,13 @@ +using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Web.Common.Controllers; + +namespace Umbraco.Extensions; + +public static class TypeLoaderExtensions +{ + /// + /// Gets all types implementing . + /// + public static IEnumerable GetUmbracoApiControllers(this TypeLoader typeLoader) + => typeLoader.GetTypes(); +} diff --git a/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs b/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs index 7edb173b2171..aacf8582b2eb 100644 --- a/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs @@ -21,6 +21,236 @@ namespace Umbraco.Extensions; public static class UrlHelperExtensions { + /// + /// Gets the Umbraco backoffice URL (if Umbraco is installed). + /// + /// The URL helper. + /// + /// The Umbraco backoffice URL. + /// + public static string? GetUmbracoBackOfficeUrl(this IUrlHelper urlHelper) + => urlHelper.Action("Default", "BackOffice", new { area = Constants.Web.Mvc.BackOfficeArea }); + + /// + /// Return the back office url if the back office is installed + /// + /// + /// + /// + /// This method contained a bug that would result in always returning "/". + /// + [Obsolete("Use the GetUmbracoBackOfficeUrl extension method instead. This method will be removed in Umbraco 13.")] + public static string? GetBackOfficeUrl(this IUrlHelper url) + => "/"; + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + /// + public static string? GetUmbracoApiService( + this IUrlHelper url, + UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, + string actionName, + object? id = null) + where T : UmbracoApiController => + url.GetUmbracoApiService(umbracoApiControllerTypeCollection, actionName, typeof(T), id); + + public static string? GetUmbracoApiService( + this IUrlHelper url, + UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, + Expression> methodSelector) + where T : UmbracoApiController + { + MethodInfo? method = ExpressionHelper.GetMethodInfo(methodSelector); + IDictionary? methodParams = ExpressionHelper.GetMethodParams(methodSelector); + if (method == null) + { + throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + + " or the result "); + } + + if (methodParams?.Any() == false) + { + return url.GetUmbracoApiService(umbracoApiControllerTypeCollection, method.Name); + } + + return url.GetUmbracoApiService(umbracoApiControllerTypeCollection, method.Name, methodParams?.Values.First()); + } + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + /// + public static string? GetUmbracoApiService( + this IUrlHelper url, + UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, + string actionName, + Type apiControllerType, + object? id = null) + { + if (actionName == null) + { + throw new ArgumentNullException(nameof(actionName)); + } + + if (string.IsNullOrWhiteSpace(actionName)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(actionName)); + } + + if (apiControllerType == null) + { + throw new ArgumentNullException(nameof(apiControllerType)); + } + + var area = string.Empty; + + Type? apiController = umbracoApiControllerTypeCollection.SingleOrDefault(x => x == apiControllerType); + if (apiController == null) + { + throw new InvalidOperationException("Could not find the umbraco api controller of type " + + apiControllerType.FullName); + } + + PluginControllerMetadata metaData = PluginController.GetMetadata(apiController); + if (metaData.AreaName.IsNullOrWhiteSpace() == false) + { + // set the area to the plugin area + area = metaData.AreaName; + } + + return url.GetUmbracoApiService(actionName, ControllerExtensions.GetControllerName(apiControllerType), area!, id); + } + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + public static string? GetUmbracoApiService(this IUrlHelper url, string actionName, string controllerName, object? id = null) => url.GetUmbracoApiService(actionName, controllerName, string.Empty, id); + + /// + /// Return the Url for a Web Api service + /// + /// + /// + /// + /// + /// + /// + public static string? GetUmbracoApiService( + this IUrlHelper url, + string actionName, + string controllerName, + string area, + object? id = null) + { + if (actionName == null) + { + throw new ArgumentNullException(nameof(actionName)); + } + + if (string.IsNullOrWhiteSpace(actionName)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(actionName)); + } + + if (controllerName == null) + { + throw new ArgumentNullException(nameof(controllerName)); + } + + if (string.IsNullOrWhiteSpace(controllerName)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(controllerName)); + } + + if (area.IsNullOrWhiteSpace()) + { + if (id == null) + { + return url.Action(actionName, controllerName); + } + + return url.Action(actionName, controllerName, new { id }); + } + + if (id == null) + { + return url.Action(actionName, controllerName, new { area }); + } + + return url.Action(actionName, controllerName, new { area, id }); + } + + /// + /// Return the Base Url (not including the action) for a Web Api service + /// + /// + /// + /// + /// + /// + public static string? GetUmbracoApiServiceBaseUrl( + this IUrlHelper url, + UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, + string actionName) + where T : UmbracoApiController => + url.GetUmbracoApiService(umbracoApiControllerTypeCollection, actionName)?.TrimEnd(actionName); + + public static string? GetUmbracoApiServiceBaseUrl( + this IUrlHelper url, + UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, + Expression> methodSelector) + where T : UmbracoApiController + { + MethodInfo? method = ExpressionHelper.GetMethodInfo(methodSelector); + if (method == null) + { + throw new MissingMethodException("Could not find the method " + methodSelector + " on type " + typeof(T) + + " or the result "); + } + + return url.GetUmbracoApiService(umbracoApiControllerTypeCollection, method.Name)?.TrimEnd(method.Name); + } + + /// + /// Return the Url for an action with a cache-busting hash appended + /// + /// + public static string GetUrlWithCacheBust( + this IUrlHelper url, + string actionName, + string controllerName, + RouteValueDictionary routeVals, + IHostingEnvironment hostingEnvironment, + IUmbracoVersion umbracoVersion) + { + var applicationJs = url.Action(actionName, controllerName, routeVals); + applicationJs = applicationJs + "?umb__rnd=" + + GetCacheBustHash(hostingEnvironment, umbracoVersion); + return applicationJs; + } + /// /// /// diff --git a/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs b/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs index a8e2fc63dc5f..1ba9a5252627 100644 --- a/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs +++ b/src/Umbraco.Web.Common/Security/ConfigureMemberCookieOptions.cs @@ -60,7 +60,17 @@ public void Configure(CookieAuthenticationOptions options) }, OnRedirectToAccessDenied = ctx => { - new CookieAuthenticationEvents().OnRedirectToAccessDenied(ctx); + // When the controller is an UmbracoAPIController, we want to return a StatusCode instead of a redirect. + // All other cases should use the default Redirect of the CookieAuthenticationEvent. + var controllerDescriptor = ctx.HttpContext.GetEndpoint()?.Metadata + .OfType() + .FirstOrDefault(); + + if (!controllerDescriptor?.ControllerTypeInfo.IsSubclassOf(typeof(UmbracoApiController)) ?? false) + { + new CookieAuthenticationEvents().OnRedirectToAccessDenied(ctx); + } + return Task.CompletedTask; }, }; diff --git a/src/Umbraco.Web.Website/Extensions/TypeLoaderExtensions.cs b/src/Umbraco.Web.Website/Extensions/TypeLoaderExtensions.cs index f0709d99c26f..f088e53a9a0d 100644 --- a/src/Umbraco.Web.Website/Extensions/TypeLoaderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/TypeLoaderExtensions.cs @@ -15,4 +15,10 @@ public static class TypeLoaderExtensions /// internal static IEnumerable GetSurfaceControllers(this TypeLoader typeLoader) => typeLoader.GetTypes(); + + /// + /// Gets all types implementing . + /// + internal static IEnumerable GetUmbracoApiControllers(this TypeLoader typeLoader) + => typeLoader.GetTypes(); } diff --git a/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs b/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs index 9b992b8276db..8c0e36a40fe9 100644 --- a/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs +++ b/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs @@ -17,6 +17,7 @@ namespace Umbraco.Cms.Web.Website.Routing; /// public sealed class FrontEndRoutes : IAreaRoutes { + private readonly UmbracoApiControllerTypeCollection _apiControllers; private readonly IRuntimeState _runtimeState; private readonly SurfaceControllerTypeCollection _surfaceControllerTypeCollection; private readonly string _umbracoPathSegment; @@ -28,10 +29,12 @@ public FrontEndRoutes( IOptions globalSettings, IHostingEnvironment hostingEnvironment, IRuntimeState runtimeState, - SurfaceControllerTypeCollection surfaceControllerTypeCollection) + SurfaceControllerTypeCollection surfaceControllerTypeCollection, + UmbracoApiControllerTypeCollection apiControllers) { _runtimeState = runtimeState; _surfaceControllerTypeCollection = surfaceControllerTypeCollection; + _apiControllers = apiControllers; _umbracoPathSegment = globalSettings.Value.GetUmbracoMvcArea(hostingEnvironment); } @@ -45,6 +48,7 @@ public void CreateRoutes(IEndpointRouteBuilder endpoints) case RuntimeLevel.Run: AutoRouteSurfaceControllers(endpoints); + AutoRouteFrontEndApiControllers(endpoints); break; case RuntimeLevel.BootFailed: case RuntimeLevel.Unknown: @@ -71,4 +75,28 @@ private void AutoRouteSurfaceControllers(IEndpointRouteBuilder endpoints) meta.AreaName); } } + + /// + /// Auto-routes all front-end api controllers + /// + private void AutoRouteFrontEndApiControllers(IEndpointRouteBuilder endpoints) + { + foreach (Type controller in _apiControllers) + { + PluginControllerMetadata meta = PluginController.GetMetadata(controller); + + // exclude back-end api controllers + if (meta.IsBackOffice) + { + continue; + } + + endpoints.MapUmbracoApiRoute( + meta.ControllerType, + _umbracoPathSegment, + meta.AreaName, + meta.IsBackOffice, + string.Empty); // no default action (this is what we had before) + } + } } diff --git a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs index fef38f2ef9b6..dbb5e436e9b2 100644 --- a/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs +++ b/tests/Umbraco.Tests.Integration/TestServerTest/UmbracoTestServerTestBase.cs @@ -105,6 +105,18 @@ public void Setup() }); } + /// + /// Prepare a url before using . + /// This returns the url but also sets the HttpContext.request into to use this url. + /// + /// The string URL of the controller action. + protected string PrepareApiControllerUrl(Expression> methodSelector) + where T : UmbracoApiController + { + var url = LinkGenerator.GetUmbracoApiService(methodSelector); + return PrepareUrl(url); + } + protected string GetManagementApiUrl(Expression> methodSelector) where T : ManagementApiControllerBase { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Web.Website/Security/MemberAuthorizeTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Web.Website/Security/MemberAuthorizeTests.cs index d28cbe03f44b..35bea8bc0e17 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Web.Website/Security/MemberAuthorizeTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Web.Website/Security/MemberAuthorizeTests.cs @@ -67,6 +67,36 @@ public async Task Secure_SurfaceController_Should_Return_Redirect_WhenNotAuthori Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode); Assert.AreEqual(cookieAuthenticationOptions.Value.AccessDeniedPath.ToString(), response.Headers.Location?.AbsolutePath); } + + [Test] + [LongRunning] + public async Task Secure_ApiController_Should_Return_Unauthorized_WhenNotLoggedIn() + { + _memberManagerMock.Setup(x => x.IsLoggedIn()).Returns(false); + var url = PrepareApiControllerUrl(x => x.Secure()); + + var response = await Client.GetAsync(url); + + Assert.AreEqual(HttpStatusCode.Unauthorized, response.StatusCode); + } + + [Test] + [LongRunning] + public async Task Secure_ApiController_Should_Return_Forbidden_WhenNotAuthorized() + { + _memberManagerMock.Setup(x => x.IsLoggedIn()).Returns(true); + _memberManagerMock.Setup(x => x.IsMemberAuthorizedAsync( + It.IsAny>(), + It.IsAny>(), + It.IsAny>())) + .ReturnsAsync(false); + + var url = PrepareApiControllerUrl(x => x.Secure()); + + var response = await Client.GetAsync(url); + + Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode); + } } public class TestSurfaceController : SurfaceController @@ -91,4 +121,10 @@ public TestSurfaceController( [UmbracoMemberAuthorize] public IActionResult Secure() => NoContent(); } + + public class TestApiController : UmbracoApiController + { + [UmbracoMemberAuthorize] + public IActionResult Secure() => NoContent(); + } } diff --git a/tests/Umbraco.Tests.UnitTests/AutoFixture/Customizations/UmbracoCustomizations.cs b/tests/Umbraco.Tests.UnitTests/AutoFixture/Customizations/UmbracoCustomizations.cs index 11baa6229e37..ea794faeb7a8 100644 --- a/tests/Umbraco.Tests.UnitTests/AutoFixture/Customizations/UmbracoCustomizations.cs +++ b/tests/Umbraco.Tests.UnitTests/AutoFixture/Customizations/UmbracoCustomizations.cs @@ -47,6 +47,14 @@ public void Customize(IFixture fixture) fixture.Customize(x => x.With(settings => settings.ApplicationVirtualPath, string.Empty)); + fixture.Customize(u => u.FromFactory( + () => new BackOfficeAreaRoutes( + Options.Create(new GlobalSettings()), + Mock.Of(x => + x.ToAbsolute(It.IsAny()) == "/umbraco" && x.ApplicationVirtualPath == string.Empty), + Mock.Of(x => x.Level == RuntimeLevel.Run), + new UmbracoApiControllerTypeCollection(Enumerable.Empty)))); + fixture.Customize(u => u.FromFactory( () => new PreviewRoutes( Options.Create(new GlobalSettings()), diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/BackOfficeAreaRoutesTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/BackOfficeAreaRoutesTests.cs new file mode 100644 index 000000000000..71735ba50020 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/BackOfficeAreaRoutesTests.cs @@ -0,0 +1,89 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Controllers.Security; +using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Cms.Web.Common.Controllers; +using Umbraco.Extensions; +using static Umbraco.Cms.Core.Constants.Web.Routing; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common.Routing; + +[TestFixture] +public class BackOfficeAreaRoutesTests +{ + [TestCase(RuntimeLevel.BootFailed)] + [TestCase(RuntimeLevel.Unknown)] + [TestCase(RuntimeLevel.Boot)] + public void RuntimeState_No_Routes(RuntimeLevel level) + { + var routes = GetBackOfficeAreaRoutes(level); + var endpoints = new TestRouteBuilder(); + routes.CreateRoutes(endpoints); + + Assert.AreEqual(0, endpoints.DataSources.Count); + } + + [Test] + [TestCase(RuntimeLevel.Run)] + [TestCase(RuntimeLevel.Upgrade)] + [TestCase(RuntimeLevel.Install)] + public void RuntimeState_All_Routes(RuntimeLevel level) + { + var routes = GetBackOfficeAreaRoutes(level); + var endpoints = new TestRouteBuilder(); + routes.CreateRoutes(endpoints); + + Assert.AreEqual(1, endpoints.DataSources.Count); + var route = endpoints.DataSources.First(); + Assert.AreEqual(3, route.Endpoints.Count); + + AssertMinimalBackOfficeRoutes(route); + + var endpoint4 = (RouteEndpoint)route.Endpoints[2]; + var apiControllerName = ControllerExtensions.GetControllerName(); + Assert.AreEqual( + $"umbraco/backoffice/api/{apiControllerName.ToLowerInvariant()}/{{action}}/{{id?}}", + endpoint4.RoutePattern.RawText); + Assert.IsFalse(endpoint4.RoutePattern.Defaults.ContainsKey(AreaToken)); + Assert.IsFalse(endpoint4.RoutePattern.Defaults.ContainsKey(ActionToken)); + Assert.AreEqual(apiControllerName, endpoint4.RoutePattern.Defaults[ControllerToken]); + } + + private void AssertMinimalBackOfficeRoutes(EndpointDataSource route) + { + var endpoint1 = (RouteEndpoint)route.Endpoints[0]; + Assert.AreEqual("umbraco/{action}/{id?}", endpoint1.RoutePattern.RawText); + Assert.AreEqual("Index", endpoint1.RoutePattern.Defaults[ActionToken]); + Assert.AreEqual(ControllerExtensions.GetControllerName(), endpoint1.RoutePattern.Defaults[ControllerToken]); + } + + private BackOfficeAreaRoutes GetBackOfficeAreaRoutes(RuntimeLevel level) + { + var globalSettings = new GlobalSettings(); + var routes = new BackOfficeAreaRoutes( + Options.Create(globalSettings), + Mock.Of(x => + x.ToAbsolute(It.IsAny()) == "/umbraco" && x.ApplicationVirtualPath == string.Empty), + Mock.Of(x => x.Level == level), + new UmbracoApiControllerTypeCollection(() => new[] { typeof(Testing1Controller) })); + + return routes; + } + + [IsBackOffice] + private class Testing1Controller : UmbracoApiController + { + } +} From 76f0b6ce86a8f93370a91307a5cfe870a2a23c59 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 10 May 2024 13:16:09 +0200 Subject: [PATCH 2/5] Obsolete UmbracoApiController --- .../Routing/BackOfficeAreaRoutes.cs | 51 +++++-------------- .../UmbracoApiControllerAttribute.cs | 5 +- .../Controllers/UmbracoApiController.cs | 10 ++++ .../Controllers/UmbracoApiControllerBase.cs | 3 +- ...bracoApiControllerTypeCollectionBuilder.cs | 1 - .../UmbracoBuilderExtensions.cs | 1 + .../Routing/BackOfficeAreaRoutesTests.cs | 11 +--- 7 files changed, 28 insertions(+), 54 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs b/src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs index 69b2dd785b3e..a7fd5bd0099d 100644 --- a/src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs +++ b/src/Umbraco.Cms.Api.Management/Routing/BackOfficeAreaRoutes.cs @@ -19,9 +19,6 @@ namespace Umbraco.Cms.Api.Management.Routing; /// public sealed class BackOfficeAreaRoutes : IAreaRoutes { - private readonly UmbracoApiControllerTypeCollection _apiControllers; - private readonly GlobalSettings _globalSettings; - private readonly IHostingEnvironment _hostingEnvironment; private readonly IRuntimeState _runtimeState; private readonly string _umbracoPathSegment; @@ -31,14 +28,20 @@ public sealed class BackOfficeAreaRoutes : IAreaRoutes public BackOfficeAreaRoutes( IOptions globalSettings, IHostingEnvironment hostingEnvironment, - IRuntimeState runtimeState, - UmbracoApiControllerTypeCollection apiControllers) + IRuntimeState runtimeState) { - _globalSettings = globalSettings.Value; - _hostingEnvironment = hostingEnvironment; _runtimeState = runtimeState; - _apiControllers = apiControllers; - _umbracoPathSegment = _globalSettings.GetUmbracoMvcArea(_hostingEnvironment); + _umbracoPathSegment = globalSettings.Value.GetUmbracoMvcArea(hostingEnvironment); + } + + [Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")] + public BackOfficeAreaRoutes( + IOptions globalSettings, + IHostingEnvironment hostingEnvironment, + IRuntimeState runtimeState, + UmbracoApiControllerTypeCollection apiControllers) : this(globalSettings, hostingEnvironment, runtimeState) + { + } /// @@ -53,7 +56,6 @@ public void CreateRoutes(IEndpointRouteBuilder endpoints) case RuntimeLevel.Run: MapMinimalBackOffice(endpoints); - AutoRouteBackOfficeApiControllers(endpoints); break; case RuntimeLevel.BootFailed: case RuntimeLevel.Unknown: @@ -88,33 +90,4 @@ private void MapMinimalBackOffice(IEndpointRouteBuilder endpoints) }, constraints: new { slug = @"^(section.*|upgrade|install|oauth_complete|logout|error)$" }); } - - /// - /// Auto-routes all back office api controllers - /// - private void AutoRouteBackOfficeApiControllers(IEndpointRouteBuilder endpoints) - { - // TODO: We could investigate dynamically routing plugin controllers so we don't have to eagerly type scan for them, - // it would probably work well, see https://www.strathweb.com/2019/08/dynamic-controller-routing-in-asp-net-core-3-0/ - // will probably be what we use for front-end routing too. BTW the orig article about migrating from IRouter to endpoint - // routing for things like a CMS is here https://github.com/dotnet/aspnetcore/issues/4221 - - foreach (Type controller in _apiControllers) - { - PluginControllerMetadata meta = PluginController.GetMetadata(controller); - - // exclude front-end api controllers - if (!meta.IsBackOffice) - { - continue; - } - - endpoints.MapUmbracoApiRoute( - meta.ControllerType, - _umbracoPathSegment, - meta.AreaName, - meta.IsBackOffice, - string.Empty); // no default action (this is what we had before) - } - } } diff --git a/src/Umbraco.Web.Common/Attributes/UmbracoApiControllerAttribute.cs b/src/Umbraco.Web.Common/Attributes/UmbracoApiControllerAttribute.cs index 48d3f3404ecc..668ed3e6d5cd 100644 --- a/src/Umbraco.Web.Common/Attributes/UmbracoApiControllerAttribute.cs +++ b/src/Umbraco.Web.Common/Attributes/UmbracoApiControllerAttribute.cs @@ -2,10 +2,9 @@ namespace Umbraco.Cms.Web.Common.Attributes; -/// -/// When present on a controller then conventions will apply -/// + [AttributeUsage(AttributeTargets.Class)] +[Obsolete("No-op attribute. Will be removed in Umbraco 15.")] public sealed class UmbracoApiControllerAttribute : Attribute { } diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs index 05d7004e1ffe..115727d9ebb6 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiController.cs @@ -5,6 +5,16 @@ namespace Umbraco.Cms.Web.Common.Controllers; /// /// Provides a base class for auto-routed Umbraco API controllers. /// +[Obsolete(""" +WARNING +The UmbracoAPIController does not work exactly as in previous versions of Umbraco because serialization is now done using System.Text.Json. +Please verify your API responses still work as expect. + +We recommend using regular ASP.NET Core ApiControllers for your APIs so that OpenAPI specifications are generated. +Read more about this here: https://learn.microsoft.com/en-us/aspnet/core/web-api/ + +UmbracoAPIController will be removed in Umbraco 15. +""")] public abstract class UmbracoApiController : UmbracoApiControllerBase, IDiscoverable { /// diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs index e5cbe66cf6cb..53f5ff52f568 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerBase.cs @@ -13,8 +13,9 @@ namespace Umbraco.Cms.Web.Common.Controllers; /// These controllers are NOT auto-routed. /// The base class is which are netcore API controllers without any view support /// -[Authorize(Policy = AuthorizationPolicies.UmbracoFeatureEnabled)] // TODO: This could be part of our conventions +[Authorize(Policy = AuthorizationPolicies.UmbracoFeatureEnabled)] [UmbracoApiController] +[Obsolete("This will be removed in Umbraco 15.")] public abstract class UmbracoApiControllerBase : ControllerBase, IUmbracoFeature { /// diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs index 87a8c8e56dfc..be80e01e2009 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs @@ -6,6 +6,5 @@ namespace Umbraco.Cms.Web.Common.Controllers; public class UmbracoApiControllerTypeCollectionBuilder : TypeCollectionBuilderBase< UmbracoApiControllerTypeCollectionBuilder, UmbracoApiControllerTypeCollection, UmbracoApiController> { - // TODO: Should this only exist in the back office project? These really are only ever used for the back office AFAIK protected override UmbracoApiControllerTypeCollectionBuilder This => this; } diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 83b209d012f7..bc34221aad12 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -44,6 +44,7 @@ using Umbraco.Cms.Web.Common.AspNetCore; using Umbraco.Cms.Web.Common.Blocks; using Umbraco.Cms.Web.Common.Configuration; +using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.FileProviders; using Umbraco.Cms.Web.Common.Helpers; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/BackOfficeAreaRoutesTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/BackOfficeAreaRoutesTests.cs index 71735ba50020..c444e591ab89 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/BackOfficeAreaRoutesTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/Routing/BackOfficeAreaRoutesTests.cs @@ -47,18 +47,9 @@ public void RuntimeState_All_Routes(RuntimeLevel level) Assert.AreEqual(1, endpoints.DataSources.Count); var route = endpoints.DataSources.First(); - Assert.AreEqual(3, route.Endpoints.Count); + Assert.AreEqual(2, route.Endpoints.Count); AssertMinimalBackOfficeRoutes(route); - - var endpoint4 = (RouteEndpoint)route.Endpoints[2]; - var apiControllerName = ControllerExtensions.GetControllerName(); - Assert.AreEqual( - $"umbraco/backoffice/api/{apiControllerName.ToLowerInvariant()}/{{action}}/{{id?}}", - endpoint4.RoutePattern.RawText); - Assert.IsFalse(endpoint4.RoutePattern.Defaults.ContainsKey(AreaToken)); - Assert.IsFalse(endpoint4.RoutePattern.Defaults.ContainsKey(ActionToken)); - Assert.AreEqual(apiControllerName, endpoint4.RoutePattern.Defaults[ControllerToken]); } private void AssertMinimalBackOfficeRoutes(EndpointDataSource route) From 40250a4ca7f7d0b88c434f2584ea90dc5e689aad Mon Sep 17 00:00:00 2001 From: kjac Date: Fri, 10 May 2024 19:20:16 +0200 Subject: [PATCH 3/5] Added a few more obsoletion messages --- src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs | 1 + .../Controllers/UmbracoApiControllerTypeCollectionBuilder.cs | 1 + src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs | 4 ++++ src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs | 5 +++++ 4 files changed, 11 insertions(+) diff --git a/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs b/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs index afd6183b548a..747a5553e112 100644 --- a/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs +++ b/src/Umbraco.Core/UmbracoApiControllerTypeCollection.cs @@ -2,6 +2,7 @@ namespace Umbraco.Cms.Core; +[Obsolete("This will be removed in Umbraco 15.")] public class UmbracoApiControllerTypeCollection : BuilderCollectionBase { public UmbracoApiControllerTypeCollection(Func> items) diff --git a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs index be80e01e2009..73fd9908e9cf 100644 --- a/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs +++ b/src/Umbraco.Web.Common/Controllers/UmbracoApiControllerTypeCollectionBuilder.cs @@ -3,6 +3,7 @@ namespace Umbraco.Cms.Web.Common.Controllers; +[Obsolete("This will be removed in Umbraco 15.")] public class UmbracoApiControllerTypeCollectionBuilder : TypeCollectionBuilderBase< UmbracoApiControllerTypeCollectionBuilder, UmbracoApiControllerTypeCollection, UmbracoApiController> { diff --git a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs index 1c589ebe2a99..6ce6e36c170d 100644 --- a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs @@ -47,15 +47,18 @@ public static string GetUmbracoBackOfficeUrl(this LinkGenerator linkGenerator, I /// Return the Url for a Web Api service /// /// The + [Obsolete("This will be removed in Umbraco 15.")] public static string? GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, object? id = null) where T : UmbracoApiControllerBase => linkGenerator.GetUmbracoControllerUrl( actionName, typeof(T), new Dictionary { ["id"] = id }); + [Obsolete("This will be removed in Umbraco 15.")] public static string? GetUmbracoApiService(this LinkGenerator linkGenerator, string actionName, IDictionary? values) where T : UmbracoApiControllerBase => linkGenerator.GetUmbracoControllerUrl(actionName, typeof(T), values); + [Obsolete("This will be removed in Umbraco 15.")] public static string? GetUmbracoApiServiceBaseUrl( this LinkGenerator linkGenerator, Expression> methodSelector) @@ -160,6 +163,7 @@ public static string GetUmbracoBackOfficeUrl(this LinkGenerator linkGenerator, I return linkGenerator.GetUmbracoControllerUrl(actionName, ControllerExtensions.GetControllerName(controllerType), area, values); } + [Obsolete("This will be removed in Umbraco 15.")] public static string? GetUmbracoApiService( this LinkGenerator linkGenerator, Expression> methodSelector) diff --git a/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs b/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs index aacf8582b2eb..9f24169f09bd 100644 --- a/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs @@ -52,6 +52,7 @@ public static class UrlHelperExtensions /// /// /// + [Obsolete("This will be removed in Umbraco 15.")] public static string? GetUmbracoApiService( this IUrlHelper url, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, @@ -60,6 +61,7 @@ public static class UrlHelperExtensions where T : UmbracoApiController => url.GetUmbracoApiService(umbracoApiControllerTypeCollection, actionName, typeof(T), id); + [Obsolete("This will be removed in Umbraco 15.")] public static string? GetUmbracoApiService( this IUrlHelper url, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, @@ -91,6 +93,7 @@ public static class UrlHelperExtensions /// /// /// + [Obsolete("This will be removed in Umbraco 15.")] public static string? GetUmbracoApiService( this IUrlHelper url, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, @@ -210,6 +213,7 @@ public static class UrlHelperExtensions /// /// /// + [Obsolete("This will be removed in Umbraco 15.")] public static string? GetUmbracoApiServiceBaseUrl( this IUrlHelper url, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, @@ -217,6 +221,7 @@ public static class UrlHelperExtensions where T : UmbracoApiController => url.GetUmbracoApiService(umbracoApiControllerTypeCollection, actionName)?.TrimEnd(actionName); + [Obsolete("This will be removed in Umbraco 15.")] public static string? GetUmbracoApiServiceBaseUrl( this IUrlHelper url, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, From 517b5e5e78b5ef5c9877157c1b196bb29f10560f Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Fri, 10 May 2024 21:22:04 +0200 Subject: [PATCH 4/5] Removed some of the reintroduced stuff again --- .../Extensions/LinkGeneratorExtensions.cs | 31 ------------------- .../Extensions/UrlHelperExtensions.cs | 22 ------------- 2 files changed, 53 deletions(-) diff --git a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs index 6ce6e36c170d..276925e79667 100644 --- a/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/LinkGeneratorExtensions.cs @@ -12,37 +12,6 @@ namespace Umbraco.Extensions; public static class LinkGeneratorExtensions { - /// - /// Gets the Umbraco backoffice URL (if Umbraco is installed). - /// - /// The link generator. - /// - /// The Umbraco backoffice URL. - /// - public static string? GetUmbracoBackOfficeUrl(this LinkGenerator linkGenerator) - => linkGenerator.GetPathByAction("Default", "BackOffice", new { area = Constants.Web.Mvc.BackOfficeArea }); - - /// - /// Gets the Umbraco backoffice URL (if Umbraco is installed) or application virtual path (in most cases just "/"). - /// - /// The link generator. - /// The hosting environment. - /// - /// The Umbraco backoffice URL. - /// - public static string GetUmbracoBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment) - => GetUmbracoBackOfficeUrl(linkGenerator) ?? hostingEnvironment.ApplicationVirtualPath; - - /// - /// Return the back office url if the back office is installed - /// - /// - /// This method contained a bug that would result in always returning "/". - /// - [Obsolete("Use the GetUmbracoBackOfficeUrl extension method instead. This method will be removed in Umbraco 13.")] - public static string? GetBackOfficeUrl(this LinkGenerator linkGenerator, IHostingEnvironment hostingEnvironment) - => "/"; - /// /// Return the Url for a Web Api service /// diff --git a/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs b/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs index 9f24169f09bd..34bc870f9484 100644 --- a/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs +++ b/src/Umbraco.Web.Common/Extensions/UrlHelperExtensions.cs @@ -21,28 +21,6 @@ namespace Umbraco.Extensions; public static class UrlHelperExtensions { - /// - /// Gets the Umbraco backoffice URL (if Umbraco is installed). - /// - /// The URL helper. - /// - /// The Umbraco backoffice URL. - /// - public static string? GetUmbracoBackOfficeUrl(this IUrlHelper urlHelper) - => urlHelper.Action("Default", "BackOffice", new { area = Constants.Web.Mvc.BackOfficeArea }); - - /// - /// Return the back office url if the back office is installed - /// - /// - /// - /// - /// This method contained a bug that would result in always returning "/". - /// - [Obsolete("Use the GetUmbracoBackOfficeUrl extension method instead. This method will be removed in Umbraco 13.")] - public static string? GetBackOfficeUrl(this IUrlHelper url) - => "/"; - /// /// Return the Url for a Web Api service /// From b260c2eb267d9cf6e321f6745f4d66f2ae744a8d Mon Sep 17 00:00:00 2001 From: kjac Date: Sun, 12 May 2024 14:32:40 +0200 Subject: [PATCH 5/5] Add obsoletion to FrontEndRoutes controller --- .../Routing/FrontEndRoutes.cs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs b/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs index 8c0e36a40fe9..c67a5acfa4c0 100644 --- a/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs +++ b/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web.Mvc; @@ -22,9 +24,7 @@ public sealed class FrontEndRoutes : IAreaRoutes private readonly SurfaceControllerTypeCollection _surfaceControllerTypeCollection; private readonly string _umbracoPathSegment; - /// - /// Initializes a new instance of the class. - /// + [Obsolete("Use non-obsolete constructor. This will be removed in Umbraco 15.")] public FrontEndRoutes( IOptions globalSettings, IHostingEnvironment hostingEnvironment, @@ -38,6 +38,23 @@ public FrontEndRoutes( _umbracoPathSegment = globalSettings.Value.GetUmbracoMvcArea(hostingEnvironment); } + /// + /// Initializes a new instance of the class. + /// + public FrontEndRoutes( + IOptions globalSettings, + IHostingEnvironment hostingEnvironment, + IRuntimeState runtimeState, + SurfaceControllerTypeCollection surfaceControllerTypeCollection) + : this( + globalSettings, + hostingEnvironment, + runtimeState, + surfaceControllerTypeCollection, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + /// public void CreateRoutes(IEndpointRouteBuilder endpoints) {