diff --git a/.globalconfig b/.globalconfig index 8c0929382d..89261f5867 100644 --- a/.globalconfig +++ b/.globalconfig @@ -50,6 +50,7 @@ dotnet_analyzer_diagnostic.category-StyleCop.CSharp.LayoutRules.severity = sugge dotnet_diagnostic.SA1636.severity = none # SA1636: File header copyright text should match dotnet_diagnostic.SA1101.severity = none # PrefixLocalCallsWithThis - stylecop appears to be ignoring dotnet_style_qualification_for_* +dotnet_diagnostic.SA1309.severity = none # FieldNamesMustNotBeginWithUnderscore dotnet_diagnostic.SA1503.severity = warning # BracesMustNotBeOmitted dotnet_diagnostic.SA1117.severity = warning # ParametersMustBeOnSameLineOrSeparateLines @@ -70,8 +71,8 @@ dotnet_diagnostic.SA1132.severity = warning # DoNotCombineFields dotnet_diagnostic.SA1134.severity = warning # AttributesMustNotShareLine dotnet_diagnostic.SA1106.severity = warning # CodeMustNotContainEmptyStatements dotnet_diagnostic.SA1312.severity = warning # VariableNamesMustBeginWithLowerCaseLetter -dotnet_diagnostic.SA1303.severity = warning # ConstFieldNamesMustBeginWithUpperCaseLetter dotnet_diagnostic.SA1310.severity = warning # FieldNamesMustNotContainUnderscore +dotnet_diagnostic.SA1303.severity = warning # ConstFieldNamesMustBeginWithUpperCaseLetter dotnet_diagnostic.SA1130.severity = warning # UseLambdaSyntax dotnet_diagnostic.SA1405.severity = warning # DebugAssertMustProvideMessageText dotnet_diagnostic.SA1205.severity = warning # PartialElementsMustDeclareAccess @@ -79,4 +80,4 @@ dotnet_diagnostic.SA1306.severity = warning # FieldNamesMustBeginWithLowerCaseLe dotnet_diagnostic.SA1209.severity = warning # UsingAliasDirectivesMustBePlacedAfterOtherUsingDirectives dotnet_diagnostic.SA1216.severity = warning # UsingStaticDirectivesMustBePlacedAtTheCorrectLocation dotnet_diagnostic.SA1133.severity = warning # DoNotCombineAttributes -dotnet_diagnostic.SA1135.severity = warning # UsingDirectivesMustBeQualified +dotnet_diagnostic.SA1135.severity = warning # UsingDirectivesMustBeQualified \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Composers/ControllersAsServicesComposer.cs b/src/Umbraco.Web.UI/Composers/ControllersAsServicesComposer.cs index 042343df67..d70a2183f4 100644 --- a/src/Umbraco.Web.UI/Composers/ControllersAsServicesComposer.cs +++ b/src/Umbraco.Web.UI/Composers/ControllersAsServicesComposer.cs @@ -1,10 +1,13 @@ -using System; -using System.Linq; using Microsoft.AspNetCore.Mvc.Controllers; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Web.Website.Controllers; +using Umbraco.Extensions; +using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.Web.UI.Composers { @@ -55,6 +58,7 @@ namespace Umbraco.Cms.Web.UI.Composers builder.Services.TryAddTransient(controller, controller); } + builder.Services.AddUnique(x => new RenderNoContentController(x.GetService()!, x.GetService>()!, x.GetService()!)); return builder; } } diff --git a/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs index 97d38bbe53..e94c2b6a0b 100644 --- a/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs +++ b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Routing; @@ -10,130 +8,143 @@ using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Web; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Website.ActionResults +namespace Umbraco.Cms.Web.Website.ActionResults; + +/// +/// Redirects to an Umbraco page by Id or Entity +/// +public class RedirectToUmbracoPageResult : IKeepTempDataResult { + private readonly IPublishedUrlProvider _publishedUrlProvider; + private readonly QueryString _queryString; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private IPublishedContent? _publishedContent; + private string? _url; + /// - /// Redirects to an Umbraco page by Id or Entity + /// Initializes a new instance of the class. /// - public class RedirectToUmbracoPageResult : IActionResult, IKeepTempDataResult + public RedirectToUmbracoPageResult( + Guid key, + IPublishedUrlProvider publishedUrlProvider, + IUmbracoContextAccessor umbracoContextAccessor) { - private IPublishedContent? _publishedContent; - private readonly QueryString _queryString; - private readonly IPublishedUrlProvider _publishedUrlProvider; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private string? _url; + Key = key; + _publishedUrlProvider = publishedUrlProvider; + _umbracoContextAccessor = umbracoContextAccessor; + } - /// - /// Initializes a new instance of the class. - /// - public RedirectToUmbracoPageResult(Guid key, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) - { - Key = key; - _publishedUrlProvider = publishedUrlProvider; - _umbracoContextAccessor = umbracoContextAccessor; - } + /// + /// Initializes a new instance of the class. + /// + public RedirectToUmbracoPageResult( + Guid key, + QueryString queryString, + IPublishedUrlProvider publishedUrlProvider, + IUmbracoContextAccessor umbracoContextAccessor) + { + Key = key; + _queryString = queryString; + _publishedUrlProvider = publishedUrlProvider; + _umbracoContextAccessor = umbracoContextAccessor; + } - /// - /// Initializes a new instance of the class. - /// - public RedirectToUmbracoPageResult(Guid key, QueryString queryString, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) - { - Key = key; - _queryString = queryString; - _publishedUrlProvider = publishedUrlProvider; - _umbracoContextAccessor = umbracoContextAccessor; - } + /// + /// Initializes a new instance of the class. + /// + public RedirectToUmbracoPageResult( + IPublishedContent? publishedContent, + IPublishedUrlProvider publishedUrlProvider, + IUmbracoContextAccessor umbracoContextAccessor) + { + _publishedContent = publishedContent; + Key = publishedContent?.Key ?? Guid.Empty; + _publishedUrlProvider = publishedUrlProvider; + _umbracoContextAccessor = umbracoContextAccessor; + } - /// - /// Initializes a new instance of the class. - /// - public RedirectToUmbracoPageResult(IPublishedContent? publishedContent, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) - { - _publishedContent = publishedContent; - Key = publishedContent?.Key ?? Guid.Empty; - _publishedUrlProvider = publishedUrlProvider; - _umbracoContextAccessor = umbracoContextAccessor; - } + /// + /// Initializes a new instance of the class. + /// + public RedirectToUmbracoPageResult( + IPublishedContent? publishedContent, + QueryString queryString, + IPublishedUrlProvider publishedUrlProvider, + IUmbracoContextAccessor umbracoContextAccessor) + { + _publishedContent = publishedContent; + Key = publishedContent?.Key ?? Guid.Empty; + _queryString = queryString; + _publishedUrlProvider = publishedUrlProvider; + _umbracoContextAccessor = umbracoContextAccessor; + } - /// - /// Initializes a new instance of the class. - /// - public RedirectToUmbracoPageResult(IPublishedContent? publishedContent, QueryString queryString, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor) - { - _publishedContent = publishedContent; - Key = publishedContent?.Key ?? Guid.Empty; - _queryString = queryString; - _publishedUrlProvider = publishedUrlProvider; - _umbracoContextAccessor = umbracoContextAccessor; - } + public Guid Key { get; } - private string Url + private string Url + { + get { - get + if (!string.IsNullOrWhiteSpace(_url)) { - if (!string.IsNullOrWhiteSpace(_url)) - { - return _url; - } - - if (PublishedContent is null) - { - throw new InvalidOperationException($"Cannot redirect, no entity was found for key {Key}"); - } - - var result = _publishedUrlProvider.GetUrl(PublishedContent.Id); - - if (result == "#") - { - throw new InvalidOperationException( - $"Could not route to entity with key {Key}, the NiceUrlProvider could not generate a URL"); - } - - _url = result; - return _url; } - } - public Guid Key { get; } - - private IPublishedContent? PublishedContent - { - get + if (PublishedContent is null) { - if (!(_publishedContent is null)) - { - return _publishedContent; - } - - // need to get the URL for the page - _publishedContent = _umbracoContextAccessor.GetRequiredUmbracoContext().Content?.GetById(Key); - - return _publishedContent; - } - } - - /// - public Task ExecuteResultAsync(ActionContext context) - { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); + throw new InvalidOperationException($"Cannot redirect, no entity was found for key {Key}"); } - HttpContext httpContext = context.HttpContext; - IUrlHelperFactory urlHelperFactory = httpContext.RequestServices.GetRequiredService(); - IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(context); - string destinationUrl = urlHelper.Content(Url); + var result = _publishedUrlProvider.GetUrl(PublishedContent.Id); - if (_queryString.HasValue) + if (result == "#") { - destinationUrl += _queryString.ToUriComponent(); + throw new InvalidOperationException( + $"Could not route to entity with key {Key}, the NiceUrlProvider could not generate a URL"); } - httpContext.Response.Redirect(destinationUrl); + _url = result; - return Task.CompletedTask; + return _url; } } + + private IPublishedContent? PublishedContent + { + get + { + if (!(_publishedContent is null)) + { + return _publishedContent; + } + + // need to get the URL for the page + _publishedContent = _umbracoContextAccessor.GetRequiredUmbracoContext().Content?.GetById(Key); + + return _publishedContent; + } + } + + /// + public Task ExecuteResultAsync(ActionContext context) + { + if (context is null) + { + throw new ArgumentNullException(nameof(context)); + } + + HttpContext httpContext = context.HttpContext; + IUrlHelperFactory urlHelperFactory = httpContext.RequestServices.GetRequiredService(); + IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(context); + var destinationUrl = urlHelper.Content(Url); + + if (_queryString.HasValue) + { + destinationUrl += _queryString.ToUriComponent(); + } + + httpContext.Response.Redirect(destinationUrl); + + return Task.CompletedTask; + } } diff --git a/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoUrlResult.cs b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoUrlResult.cs index 4857c9c9a1..115096cfa6 100644 --- a/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoUrlResult.cs +++ b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoUrlResult.cs @@ -1,41 +1,38 @@ -using System; -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Umbraco.Cms.Core.Web; -namespace Umbraco.Cms.Web.Website.ActionResults +namespace Umbraco.Cms.Web.Website.ActionResults; + +/// +/// Redirects to the current URL rendering an Umbraco page including it's query strings +/// +/// +/// This is useful if you need to redirect +/// to the current page but the current page is actually a rewritten URL normally done with something like +/// Server.Transfer. It is also handy if you want to persist the query strings. +/// +public class RedirectToUmbracoUrlResult : IKeepTempDataResult { + private readonly IUmbracoContext _umbracoContext; + /// - /// Redirects to the current URL rendering an Umbraco page including it's query strings + /// Initializes a new instance of the class. /// - /// - /// This is useful if you need to redirect - /// to the current page but the current page is actually a rewritten URL normally done with something like - /// Server.Transfer. It is also handy if you want to persist the query strings. - /// - public class RedirectToUmbracoUrlResult : IActionResult, IKeepTempDataResult + public RedirectToUmbracoUrlResult(IUmbracoContext umbracoContext) => _umbracoContext = umbracoContext; + + /// + public Task ExecuteResultAsync(ActionContext context) { - private readonly IUmbracoContext _umbracoContext; - - /// - /// Initializes a new instance of the class. - /// - public RedirectToUmbracoUrlResult(IUmbracoContext umbracoContext) => _umbracoContext = umbracoContext; - - /// - public Task ExecuteResultAsync(ActionContext context) + if (context is null) { - if (context is null) - { - throw new ArgumentNullException(nameof(context)); - } - - var destinationUrl = _umbracoContext.OriginalRequestUrl.PathAndQuery; - - context.HttpContext.Response.Redirect(destinationUrl); - - return Task.CompletedTask; + throw new ArgumentNullException(nameof(context)); } + + var destinationUrl = _umbracoContext.OriginalRequestUrl.PathAndQuery; + + context.HttpContext.Response.Redirect(destinationUrl); + + return Task.CompletedTask; } } diff --git a/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs b/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs index 29fe9eaf60..897aca28bb 100644 --- a/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs +++ b/src/Umbraco.Web.Website/ActionResults/UmbracoPageResult.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -10,60 +8,65 @@ using Umbraco.Cms.Web.Common.Routing; using Umbraco.Cms.Web.Website.Controllers; using static Umbraco.Cms.Core.Constants.Web.Routing; -namespace Umbraco.Cms.Web.Website.ActionResults +namespace Umbraco.Cms.Web.Website.ActionResults; + +/// +/// Used by posted forms to proxy the result to the page in which the current URL matches on +/// +/// +/// This page does not redirect therefore it does not implement because TempData +/// should +/// only be used in situations when a redirect occurs. It is not good practice to use TempData when redirects do not +/// occur +/// so we'll be strict about it and not save it. +/// +public class UmbracoPageResult : IActionResult { + private readonly IProfilingLogger _profilingLogger; + /// - /// Used by posted forms to proxy the result to the page in which the current URL matches on + /// Initializes a new instance of the class. /// - /// - /// This page does not redirect therefore it does not implement because TempData should - /// only be used in situations when a redirect occurs. It is not good practice to use TempData when redirects do not occur - /// so we'll be strict about it and not save it. - /// - public class UmbracoPageResult : IActionResult + public UmbracoPageResult(IProfilingLogger profilingLogger) => _profilingLogger = profilingLogger; + + /// + public async Task ExecuteResultAsync(ActionContext context) { - private readonly IProfilingLogger _profilingLogger; - - /// - /// Initializes a new instance of the class. - /// - public UmbracoPageResult(IProfilingLogger profilingLogger) => _profilingLogger = profilingLogger; - - /// - public async Task ExecuteResultAsync(ActionContext context) + UmbracoRouteValues? umbracoRouteValues = context.HttpContext.Features.Get(); + if (umbracoRouteValues == null) { - UmbracoRouteValues? umbracoRouteValues = context.HttpContext.Features.Get(); - if (umbracoRouteValues == null) - { - throw new InvalidOperationException($"Can only use {nameof(UmbracoPageResult)} in the context of an Http POST when using a {nameof(SurfaceController)} form"); - } - - // Change the route values back to the original request vals - context.RouteData.Values[ControllerToken] = umbracoRouteValues.ControllerName; - context.RouteData.Values[ActionToken] = umbracoRouteValues.ActionName; - - // Create a new context and excute the original controller... - // Copy the action context - this also copies the ModelState - var renderActionContext = new ActionContext(context) - { - ActionDescriptor = umbracoRouteValues.ControllerActionDescriptor - }; - IActionInvokerFactory actionInvokerFactory = context.HttpContext.RequestServices.GetRequiredService(); - IActionInvoker? actionInvoker = actionInvokerFactory.CreateInvoker(renderActionContext); - await ExecuteControllerAction(actionInvoker); + throw new InvalidOperationException( + $"Can only use {nameof(UmbracoPageResult)} in the context of an Http POST when using a {nameof(SurfaceController)} form"); } - /// - /// Executes the controller action - /// - private async Task ExecuteControllerAction(IActionInvoker? actionInvoker) + // Change the route values back to the original request vals + context.RouteData.Values[ControllerToken] = umbracoRouteValues.ControllerName; + context.RouteData.Values[ActionToken] = umbracoRouteValues.ActionName; + + // Create a new context and excute the original controller... + // Copy the action context - this also copies the ModelState + var renderActionContext = new ActionContext(context) { - using (_profilingLogger.TraceDuration("Executing Umbraco RouteDefinition controller", "Finished")) + ActionDescriptor = umbracoRouteValues.ControllerActionDescriptor, + }; + IActionInvokerFactory actionInvokerFactory = + context.HttpContext.RequestServices.GetRequiredService(); + IActionInvoker? actionInvoker = actionInvokerFactory.CreateInvoker(renderActionContext); + await ExecuteControllerAction(actionInvoker); + } + + /// + /// Executes the controller action + /// + private async Task ExecuteControllerAction(IActionInvoker? actionInvoker) + { + using (_profilingLogger.TraceDuration( + "Executing Umbraco RouteDefinition controller", + "Finished")) + { + if (actionInvoker is not null) { - if (actionInvoker is not null) - { - await actionInvoker.InvokeAsync(); - } + await actionInvoker.InvokeAsync(); } } } diff --git a/src/Umbraco.Web.Website/Collections/SurfaceControllerTypeCollection.cs b/src/Umbraco.Web.Website/Collections/SurfaceControllerTypeCollection.cs index 95c9208df6..736b64ac5a 100644 --- a/src/Umbraco.Web.Website/Collections/SurfaceControllerTypeCollection.cs +++ b/src/Umbraco.Web.Website/Collections/SurfaceControllerTypeCollection.cs @@ -1,13 +1,11 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; -namespace Umbraco.Cms.Web.Website.Collections +namespace Umbraco.Cms.Web.Website.Collections; + +public class SurfaceControllerTypeCollection : BuilderCollectionBase { - public class SurfaceControllerTypeCollection : BuilderCollectionBase + public SurfaceControllerTypeCollection(Func> items) + : base(items) { - public SurfaceControllerTypeCollection(Func> items) : base(items) - { - } } } diff --git a/src/Umbraco.Web.Website/Collections/SurfaceControllerTypeCollectionBuilder.cs b/src/Umbraco.Web.Website/Collections/SurfaceControllerTypeCollectionBuilder.cs index 17fea9077b..9194649e26 100644 --- a/src/Umbraco.Web.Website/Collections/SurfaceControllerTypeCollectionBuilder.cs +++ b/src/Umbraco.Web.Website/Collections/SurfaceControllerTypeCollectionBuilder.cs @@ -1,10 +1,10 @@ -using Umbraco.Cms.Core.Composing; +using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Web.Website.Controllers; -namespace Umbraco.Cms.Web.Website.Collections +namespace Umbraco.Cms.Web.Website.Collections; + +public class SurfaceControllerTypeCollectionBuilder : TypeCollectionBuilderBase { - public class SurfaceControllerTypeCollectionBuilder : TypeCollectionBuilderBase - { - protected override SurfaceControllerTypeCollectionBuilder This => this; - } + protected override SurfaceControllerTypeCollectionBuilder This => this; } diff --git a/src/Umbraco.Web.Website/Controllers/RenderNoContentController.cs b/src/Umbraco.Web.Website/Controllers/RenderNoContentController.cs index aa0c0afc3a..9704549e4e 100644 --- a/src/Umbraco.Web.Website/Controllers/RenderNoContentController.cs +++ b/src/Umbraco.Web.Website/Controllers/RenderNoContentController.cs @@ -1,43 +1,56 @@ -using System; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Website.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Website.Controllers +namespace Umbraco.Cms.Web.Website.Controllers; + +public class RenderNoContentController : Controller { - public class RenderNoContentController : Controller + private readonly GlobalSettings _globalSettings; + private readonly IHostingEnvironment _hostingEnvironment; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + [Obsolete("Please use constructor that takes an IHostingEnvironment instead")] + public RenderNoContentController( + IUmbracoContextAccessor umbracoContextAccessor, + IIOHelper ioHelper, + IOptionsSnapshot globalSettings) + : this(umbracoContextAccessor, globalSettings, StaticServiceProvider.Instance.GetRequiredService()) { - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IIOHelper _ioHelper; - private readonly GlobalSettings _globalSettings; + } - public RenderNoContentController(IUmbracoContextAccessor umbracoContextAccessor, IIOHelper ioHelper, IOptionsSnapshot globalSettings) + [ActivatorUtilitiesConstructor] + public RenderNoContentController( + IUmbracoContextAccessor umbracoContextAccessor, + IOptionsSnapshot globalSettings, + IHostingEnvironment hostingEnvironment) + { + _umbracoContextAccessor = + umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _hostingEnvironment = hostingEnvironment; + _globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings)); + } + + public ActionResult Index() + { + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IPublishedContentCache? store = umbracoContext.Content; + if (store?.HasContent() ?? false) { - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); - _ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper)); - _globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings)); + // If there is actually content, go to the root. + return Redirect("~/"); } - public ActionResult Index() - { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - var store = umbracoContext.Content; - if (store?.HasContent() ?? false) - { - // If there is actually content, go to the root. - return Redirect("~/"); - } + var model = new NoNodesViewModel { UmbracoPath = _hostingEnvironment.ToAbsolute(_globalSettings.UmbracoPath) }; - var model = new NoNodesViewModel - { - UmbracoPath = _ioHelper.ResolveUrl(_globalSettings.UmbracoPath), - }; - - return View(_globalSettings.NoNodesViewPath, model); - } + return View(_globalSettings.NoNodesViewPath, model); } } diff --git a/src/Umbraco.Web.Website/Controllers/SurfaceController.cs b/src/Umbraco.Web.Website/Controllers/SurfaceController.cs index a8e875bcf7..e32edd2fe3 100644 --- a/src/Umbraco.Web.Website/Controllers/SurfaceController.cs +++ b/src/Umbraco.Web.Website/Controllers/SurfaceController.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core.Cache; @@ -12,94 +11,102 @@ using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.Routing; using Umbraco.Cms.Web.Website.ActionResults; -namespace Umbraco.Cms.Web.Website.Controllers +namespace Umbraco.Cms.Web.Website.Controllers; + +/// +/// Provides a base class for front-end add-in controllers. +/// +[AutoValidateAntiforgeryToken] +public abstract class SurfaceController : PluginController { /// - /// Provides a base class for front-end add-in controllers. + /// Initializes a new instance of the class. /// - [AutoValidateAntiforgeryToken] - public abstract class SurfaceController : PluginController + protected SurfaceController( + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider) + : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger) + => PublishedUrlProvider = publishedUrlProvider; + + protected IPublishedUrlProvider PublishedUrlProvider { get; } + + /// + /// Gets the current page. + /// + protected virtual IPublishedContent? CurrentPage { - /// - /// Initializes a new instance of the class. - /// - protected SurfaceController(IUmbracoContextAccessor umbracoContextAccessor, IUmbracoDatabaseFactory databaseFactory, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger, IPublishedUrlProvider publishedUrlProvider) - : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger) - => PublishedUrlProvider = publishedUrlProvider; - - protected IPublishedUrlProvider PublishedUrlProvider { get; } - - /// - /// Gets the current page. - /// - protected virtual IPublishedContent? CurrentPage + get { - get + UmbracoRouteValues? umbracoRouteValues = HttpContext.Features.Get(); + if (umbracoRouteValues is null) { - UmbracoRouteValues? umbracoRouteValues = HttpContext.Features.Get(); - if (umbracoRouteValues is null) - { - throw new InvalidOperationException($"No {nameof(UmbracoRouteValues)} feature was found in the HttpContext"); - } - - return umbracoRouteValues.PublishedRequest.PublishedContent; + throw new InvalidOperationException( + $"No {nameof(UmbracoRouteValues)} feature was found in the HttpContext"); } - } - /// - /// Redirects to the Umbraco page with the given id - /// - protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid contentKey) - => new RedirectToUmbracoPageResult(contentKey, PublishedUrlProvider, UmbracoContextAccessor); - - /// - /// Redirects to the Umbraco page with the given id and passes provided querystring - /// - protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid contentKey, QueryString queryString) - => new RedirectToUmbracoPageResult(contentKey, queryString, PublishedUrlProvider, UmbracoContextAccessor); - - /// - /// Redirects to the Umbraco page with the given published content - /// - protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent) - => new RedirectToUmbracoPageResult(publishedContent, PublishedUrlProvider, UmbracoContextAccessor); - - /// - /// Redirects to the Umbraco page with the given published content and passes provided querystring - /// - protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent, QueryString queryString) - => new RedirectToUmbracoPageResult(publishedContent, queryString, PublishedUrlProvider, UmbracoContextAccessor); - - /// - /// Redirects to the currently rendered Umbraco page - /// - protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage() - => new RedirectToUmbracoPageResult(CurrentPage, PublishedUrlProvider, UmbracoContextAccessor); - - /// - /// Redirects to the currently rendered Umbraco page and passes provided querystring - /// - protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage(QueryString queryString) - => new RedirectToUmbracoPageResult(CurrentPage, queryString, PublishedUrlProvider, UmbracoContextAccessor); - - /// - /// Redirects to the currently rendered Umbraco URL - /// - /// - /// This is useful if you need to redirect - /// to the current page but the current page is actually a rewritten URL normally done with something like - /// Server.Transfer.* - /// - protected RedirectToUmbracoUrlResult RedirectToCurrentUmbracoUrl() - => new RedirectToUmbracoUrlResult(UmbracoContext); - - /// - /// Returns the currently rendered Umbraco page - /// - protected UmbracoPageResult CurrentUmbracoPage() - { - HttpContext.Features.Set(new ProxyViewDataFeature(ViewData, TempData)); - return new UmbracoPageResult(ProfilingLogger); + return umbracoRouteValues.PublishedRequest.PublishedContent; } } + + /// + /// Redirects to the Umbraco page with the given id + /// + protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid contentKey) + => new(contentKey, PublishedUrlProvider, UmbracoContextAccessor); + + /// + /// Redirects to the Umbraco page with the given id and passes provided querystring + /// + protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid contentKey, QueryString queryString) + => new(contentKey, queryString, PublishedUrlProvider, UmbracoContextAccessor); + + /// + /// Redirects to the Umbraco page with the given published content + /// + protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent) + => new(publishedContent, PublishedUrlProvider, UmbracoContextAccessor); + + /// + /// Redirects to the Umbraco page with the given published content and passes provided querystring + /// + protected RedirectToUmbracoPageResult RedirectToUmbracoPage( + IPublishedContent publishedContent, + QueryString queryString) + => new(publishedContent, queryString, PublishedUrlProvider, UmbracoContextAccessor); + + /// + /// Redirects to the currently rendered Umbraco page + /// + protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage() + => new(CurrentPage, PublishedUrlProvider, UmbracoContextAccessor); + + /// + /// Redirects to the currently rendered Umbraco page and passes provided querystring + /// + protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage(QueryString queryString) + => new(CurrentPage, queryString, PublishedUrlProvider, UmbracoContextAccessor); + + /// + /// Redirects to the currently rendered Umbraco URL + /// + /// + /// This is useful if you need to redirect + /// to the current page but the current page is actually a rewritten URL normally done with something like + /// Server.Transfer.* + /// + protected RedirectToUmbracoUrlResult RedirectToCurrentUmbracoUrl() + => new(UmbracoContext); + + /// + /// Returns the currently rendered Umbraco page + /// + protected UmbracoPageResult CurrentUmbracoPage() + { + HttpContext.Features.Set(new ProxyViewDataFeature(ViewData, TempData)); + return new UmbracoPageResult(ProfilingLogger); + } } diff --git a/src/Umbraco.Web.Website/Controllers/UmbExternalLoginController.cs b/src/Umbraco.Web.Website/Controllers/UmbExternalLoginController.cs index dc079cd605..1ea68f40aa 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbExternalLoginController.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbExternalLoginController.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using System.Linq; using System.Security.Claims; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.Extensions; @@ -12,7 +9,6 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Logging; -using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; @@ -24,266 +20,269 @@ using Umbraco.Cms.Web.Common.Security; using Umbraco.Extensions; using SignInResult = Microsoft.AspNetCore.Identity.SignInResult; -namespace Umbraco.Cms.Web.Website.Controllers +namespace Umbraco.Cms.Web.Website.Controllers; + +[UmbracoMemberAuthorize] +public class UmbExternalLoginController : SurfaceController { - [UmbracoMemberAuthorize] - public class UmbExternalLoginController : SurfaceController + private readonly ILogger _logger; + private readonly IMemberManager _memberManager; + private readonly IMemberSignInManager _memberSignInManager; + private readonly IOptions _securitySettings; + private readonly ITwoFactorLoginService _twoFactorLoginService; + + public UmbExternalLoginController( + ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IMemberSignInManager memberSignInManager, + IMemberManager memberManager, + ITwoFactorLoginService twoFactorLoginService, + IOptions securitySettings) + : base( + umbracoContextAccessor, + databaseFactory, + services, + appCaches, + profilingLogger, + publishedUrlProvider) { - private readonly IMemberManager _memberManager; - private readonly ITwoFactorLoginService _twoFactorLoginService; - private readonly IOptions _securitySettings; - private readonly ILogger _logger; - private readonly IMemberSignInManager _memberSignInManager; + _logger = logger; + _memberSignInManager = memberSignInManager; + _memberManager = memberManager; + _twoFactorLoginService = twoFactorLoginService; + _securitySettings = securitySettings; + } - public UmbExternalLoginController( - ILogger logger, - IUmbracoContextAccessor umbracoContextAccessor, - IUmbracoDatabaseFactory databaseFactory, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger profilingLogger, - IPublishedUrlProvider publishedUrlProvider, - IMemberSignInManager memberSignInManager, - IMemberManager memberManager, - ITwoFactorLoginService twoFactorLoginService, - IOptions securitySettings) - : base( - umbracoContextAccessor, - databaseFactory, - services, - appCaches, - profilingLogger, - publishedUrlProvider) + /// + /// Endpoint used to redirect to a specific login provider. This endpoint is used from the Login Macro snippet. + /// + [HttpPost] + [AllowAnonymous] + [ValidateAntiForgeryToken] + public ActionResult ExternalLogin(string provider, string? returnUrl = null) + { + if (returnUrl.IsNullOrWhiteSpace()) { - _logger = logger; - _memberSignInManager = memberSignInManager; - _memberManager = memberManager; - _twoFactorLoginService = twoFactorLoginService; - _securitySettings = securitySettings; + returnUrl = Request.GetEncodedPathAndQuery(); } - /// - /// Endpoint used to redirect to a specific login provider. This endpoint is used from the Login Macro snippet. - /// - [HttpPost] - [AllowAnonymous] - [ValidateAntiForgeryToken] - public ActionResult ExternalLogin(string provider, string? returnUrl = null) + var wrappedReturnUrl = + Url.SurfaceAction(nameof(ExternalLoginCallback), this.GetControllerName(), new { returnUrl }); + + AuthenticationProperties properties = + _memberSignInManager.ConfigureExternalAuthenticationProperties(provider, wrappedReturnUrl); + + return Challenge(properties, provider); + } + + /// + /// Endpoint used my the login provider to call back to our solution. + /// + [HttpGet] + [AllowAnonymous] + public async Task ExternalLoginCallback(string returnUrl) + { + var errors = new List(); + + ExternalLoginInfo? loginInfo = await _memberSignInManager.GetExternalLoginInfoAsync(); + if (loginInfo is null) { - if (returnUrl.IsNullOrWhiteSpace()) - { - returnUrl = Request.GetEncodedPathAndQuery(); - } - - var wrappedReturnUrl = - Url.SurfaceAction(nameof(ExternalLoginCallback), this.GetControllerName(), new { returnUrl }); - - AuthenticationProperties properties = - _memberSignInManager.ConfigureExternalAuthenticationProperties(provider, wrappedReturnUrl); - - return Challenge(properties, provider); + errors.Add("Invalid response from the login provider"); } - - /// - /// Endpoint used my the login provider to call back to our solution. - /// - [HttpGet] - [AllowAnonymous] - public async Task ExternalLoginCallback(string returnUrl) + else { - var errors = new List(); + SignInResult result = await _memberSignInManager.ExternalLoginSignInAsync( + loginInfo, + false, + _securitySettings.Value.MemberBypassTwoFactorForExternalLogins); - ExternalLoginInfo? loginInfo = await _memberSignInManager.GetExternalLoginInfoAsync(); - if (loginInfo is null) + if (result == SignInResult.Success) { - errors.Add("Invalid response from the login provider"); - } - else - { - SignInResult result = await _memberSignInManager.ExternalLoginSignInAsync(loginInfo, false, _securitySettings.Value.MemberBypassTwoFactorForExternalLogins); + // Update any authentication tokens if succeeded + await _memberSignInManager.UpdateExternalAuthenticationTokensAsync(loginInfo); - if (result == SignInResult.Success) - { - // Update any authentication tokens if succeeded - await _memberSignInManager.UpdateExternalAuthenticationTokensAsync(loginInfo); - - return RedirectToLocal(returnUrl); - } - - if (result == SignInResult.TwoFactorRequired) - { - MemberIdentityUser attemptedUser = - await _memberManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey); - if (attemptedUser == null) - { - return new ValidationErrorResult( - $"No local user found for the login provider {loginInfo.LoginProvider} - {loginInfo.ProviderKey}"); - } - - - var providerNames = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(attemptedUser.Key); - ViewData.SetTwoFactorProviderNames(providerNames); - - return CurrentUmbracoPage(); - - } - - if (result == SignInResult.LockedOut) - { - errors.Add( - $"The local member {loginInfo.Principal.Identity?.Name} for the external provider {loginInfo.ProviderDisplayName} is locked out."); - } - else if (result == SignInResult.NotAllowed) - { - // This occurs when SignInManager.CanSignInAsync fails which is when RequireConfirmedEmail , RequireConfirmedPhoneNumber or RequireConfirmedAccount fails - // however since we don't enforce those rules (yet) this shouldn't happen. - errors.Add( - $"The member {loginInfo.Principal.Identity?.Name} for the external provider {loginInfo.ProviderDisplayName} has not confirmed their details and cannot sign in."); - } - else if (result == SignInResult.Failed) - { - // Failed only occurs when the user does not exist - errors.Add("The requested provider (" + loginInfo.LoginProvider + - ") has not been linked to an account, the provider must be linked before it can be used."); - } - else if (result == MemberSignInManager.ExternalLoginSignInResult.NotAllowed) - { - // This occurs when the external provider has approved the login but custom logic in OnExternalLogin has denined it. - errors.Add( - $"The user {loginInfo.Principal.Identity?.Name} for the external provider {loginInfo.ProviderDisplayName} has not been accepted and cannot sign in."); - } - else if (result == MemberSignInManager.AutoLinkSignInResult.FailedNotLinked) - { - errors.Add("The requested provider (" + loginInfo.LoginProvider + - ") has not been linked to an account, the provider must be linked from the back office."); - } - else if (result == MemberSignInManager.AutoLinkSignInResult.FailedNoEmail) - { - errors.Add( - $"The requested provider ({loginInfo.LoginProvider}) has not provided the email claim {ClaimTypes.Email}, the account cannot be linked."); - } - else if (result is MemberSignInManager.AutoLinkSignInResult autoLinkSignInResult && - autoLinkSignInResult.Errors.Count > 0) - { - errors.AddRange(autoLinkSignInResult.Errors); - } - else if (!result.Succeeded) - { - // this shouldn't occur, the above should catch the correct error but we'll be safe just in case - errors.Add($"An unknown error with the requested provider ({loginInfo.LoginProvider}) occurred."); - } + return RedirectToLocal(returnUrl); } - if (errors.Count > 0) + if (result == SignInResult.TwoFactorRequired) { - ViewData.SetExternalSignInProviderErrors( - new BackOfficeExternalLoginProviderErrors( - loginInfo?.LoginProvider, - errors)); + MemberIdentityUser attemptedUser = + await _memberManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey); + if (attemptedUser == null!) + { + return new ValidationErrorResult( + $"No local user found for the login provider {loginInfo.LoginProvider} - {loginInfo.ProviderKey}"); + } + + IEnumerable providerNames = + await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(attemptedUser.Key); + ViewData.SetTwoFactorProviderNames(providerNames); return CurrentUmbracoPage(); } - return RedirectToLocal(returnUrl); - } - - private void AddModelErrors(IdentityResult result, string prefix = "") - { - foreach (IdentityError error in result.Errors) + if (result == SignInResult.LockedOut) { - ModelState.AddModelError(prefix, error.Description); + errors.Add( + $"The local member {loginInfo.Principal.Identity?.Name} for the external provider {loginInfo.ProviderDisplayName} is locked out."); + } + else if (result == SignInResult.NotAllowed) + { + // This occurs when SignInManager.CanSignInAsync fails which is when RequireConfirmedEmail , RequireConfirmedPhoneNumber or RequireConfirmedAccount fails + // however since we don't enforce those rules (yet) this shouldn't happen. + errors.Add( + $"The member {loginInfo.Principal.Identity?.Name} for the external provider {loginInfo.ProviderDisplayName} has not confirmed their details and cannot sign in."); + } + else if (result == SignInResult.Failed) + { + // Failed only occurs when the user does not exist + errors.Add("The requested provider (" + loginInfo.LoginProvider + + ") has not been linked to an account, the provider must be linked before it can be used."); + } + else if (result == MemberSignInManager.ExternalLoginSignInResult.NotAllowed) + { + // This occurs when the external provider has approved the login but custom logic in OnExternalLogin has denined it. + errors.Add( + $"The user {loginInfo.Principal.Identity?.Name} for the external provider {loginInfo.ProviderDisplayName} has not been accepted and cannot sign in."); + } + else if (result == MemberSignInManager.AutoLinkSignInResult.FailedNotLinked) + { + errors.Add("The requested provider (" + loginInfo.LoginProvider + + ") has not been linked to an account, the provider must be linked from the back office."); + } + else if (result == MemberSignInManager.AutoLinkSignInResult.FailedNoEmail) + { + errors.Add( + $"The requested provider ({loginInfo.LoginProvider}) has not provided the email claim {ClaimTypes.Email}, the account cannot be linked."); + } + else if (result is MemberSignInManager.AutoLinkSignInResult autoLinkSignInResult && + autoLinkSignInResult.Errors.Count > 0) + { + errors.AddRange(autoLinkSignInResult.Errors); + } + else if (!result.Succeeded) + { + // this shouldn't occur, the above should catch the correct error but we'll be safe just in case + errors.Add($"An unknown error with the requested provider ({loginInfo.LoginProvider}) occurred."); } } - [HttpPost] - [ValidateAntiForgeryToken] - public IActionResult LinkLogin(string provider, string? returnUrl = null) + if (errors.Count > 0) { - if (returnUrl.IsNullOrWhiteSpace()) - { - returnUrl = Request.GetEncodedPathAndQuery(); - } + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginInfo?.LoginProvider, + errors)); - var wrappedReturnUrl = - Url.SurfaceAction(nameof(ExternalLinkLoginCallback), this.GetControllerName(), new { returnUrl }); - - // Configures the redirect URL and user identifier for the specified external login including xsrf data - AuthenticationProperties properties = - _memberSignInManager.ConfigureExternalAuthenticationProperties(provider, wrappedReturnUrl, - _memberManager.GetUserId(User)); - - return Challenge(properties, provider); + return CurrentUmbracoPage(); } - [HttpGet] - public async Task ExternalLinkLoginCallback(string returnUrl) + return RedirectToLocal(returnUrl); + } + + private void AddModelErrors(IdentityResult result, string prefix = "") + { + foreach (IdentityError error in result.Errors) { - MemberIdentityUser user = await _memberManager.GetUserAsync(User); - string? loginProvider = null; - var errors = new List(); - if (user == null) + ModelState.AddModelError(prefix, error.Description); + } + } + + [HttpPost] + [ValidateAntiForgeryToken] + public IActionResult LinkLogin(string provider, string? returnUrl = null) + { + if (returnUrl.IsNullOrWhiteSpace()) + { + returnUrl = Request.GetEncodedPathAndQuery(); + } + + var wrappedReturnUrl = + Url.SurfaceAction(nameof(ExternalLinkLoginCallback), this.GetControllerName(), new { returnUrl }); + + // Configures the redirect URL and user identifier for the specified external login including xsrf data + AuthenticationProperties properties = + _memberSignInManager.ConfigureExternalAuthenticationProperties( + provider, + wrappedReturnUrl, + _memberManager.GetUserId(User)); + + return Challenge(properties, provider); + } + + [HttpGet] + public async Task ExternalLinkLoginCallback(string returnUrl) + { + MemberIdentityUser user = await _memberManager.GetUserAsync(User); + string? loginProvider = null; + var errors = new List(); + if (user == null!) + { + // ... this should really not happen + errors.Add("Local user does not exist"); + } + else + { + ExternalLoginInfo? info = + await _memberSignInManager.GetExternalLoginInfoAsync(await _memberManager.GetUserIdAsync(user)); + + if (info == null) { - // ... this should really not happen - errors.Add("Local user does not exist"); + // Add error and redirect for it to be displayed + errors.Add("An error occurred, could not get external login info"); } else { - ExternalLoginInfo? info = - await _memberSignInManager.GetExternalLoginInfoAsync(await _memberManager.GetUserIdAsync(user)); - - if (info == null) + loginProvider = info.LoginProvider; + IdentityResult addLoginResult = await _memberManager.AddLoginAsync(user, info); + if (addLoginResult.Succeeded) { - //Add error and redirect for it to be displayed - errors.Add( "An error occurred, could not get external login info"); - } - else - { - loginProvider = info.LoginProvider; - IdentityResult addLoginResult = await _memberManager.AddLoginAsync(user, info); - if (addLoginResult.Succeeded) - { - // Update any authentication tokens if succeeded - await _memberSignInManager.UpdateExternalAuthenticationTokensAsync(info); + // Update any authentication tokens if succeeded + await _memberSignInManager.UpdateExternalAuthenticationTokensAsync(info); - return RedirectToLocal(returnUrl); - } - - //Add errors and redirect for it to be displayed - errors.AddRange(addLoginResult.Errors.Select(x => x.Description)); + return RedirectToLocal(returnUrl); } + + // Add errors and redirect for it to be displayed + errors.AddRange(addLoginResult.Errors.Select(x => x.Description)); } - - ViewData.SetExternalSignInProviderErrors( - new BackOfficeExternalLoginProviderErrors( - loginProvider, - errors)); - return CurrentUmbracoPage(); } - private IActionResult RedirectToLocal(string returnUrl) => - Url.IsLocalUrl(returnUrl) ? Redirect(returnUrl) : RedirectToCurrentUmbracoPage(); + ViewData.SetExternalSignInProviderErrors( + new BackOfficeExternalLoginProviderErrors( + loginProvider, + errors)); + return CurrentUmbracoPage(); + } - [HttpPost] - [ValidateAntiForgeryToken] - public async Task Disassociate(string provider, string providerKey, string? returnUrl = null) + private IActionResult RedirectToLocal(string returnUrl) => + Url.IsLocalUrl(returnUrl) ? Redirect(returnUrl) : RedirectToCurrentUmbracoPage(); + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task Disassociate(string provider, string providerKey, string? returnUrl = null) + { + if (returnUrl.IsNullOrWhiteSpace()) { - if (returnUrl.IsNullOrWhiteSpace()) - { - returnUrl = Request.GetEncodedPathAndQuery(); - } - - MemberIdentityUser user = await _memberManager.FindByIdAsync(User.Identity?.GetUserId()); - - IdentityResult result = await _memberManager.RemoveLoginAsync(user, provider, providerKey); - - if (result.Succeeded) - { - await _memberSignInManager.SignInAsync(user, false); - return RedirectToLocal(returnUrl!); - } - - AddModelErrors(result); - return CurrentUmbracoPage(); + returnUrl = Request.GetEncodedPathAndQuery(); } + + MemberIdentityUser user = await _memberManager.FindByIdAsync(User.Identity?.GetUserId()); + + IdentityResult result = await _memberManager.RemoveLoginAsync(user, provider, providerKey); + + if (result.Succeeded) + { + await _memberSignInManager.SignInAsync(user, false); + return RedirectToLocal(returnUrl!); + } + + AddModelErrors(result); + return CurrentUmbracoPage(); } } diff --git a/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs b/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs index 7db6d3d702..cd9bc15e35 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbLoginController.cs @@ -1,13 +1,7 @@ -using System; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Logging; -using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; @@ -21,126 +15,130 @@ using Umbraco.Cms.Web.Common.Security; using Umbraco.Extensions; using SignInResult = Microsoft.AspNetCore.Identity.SignInResult; -namespace Umbraco.Cms.Web.Website.Controllers +namespace Umbraco.Cms.Web.Website.Controllers; + +public class UmbLoginController : SurfaceController { - public class UmbLoginController : SurfaceController + private readonly IMemberManager _memberManager; + private readonly IMemberSignInManager _signInManager; + private readonly ITwoFactorLoginService _twoFactorLoginService; + + [ActivatorUtilitiesConstructor] + public UmbLoginController( + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IMemberSignInManager signInManager, + IMemberManager memberManager, + ITwoFactorLoginService twoFactorLoginService) + : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) { - private readonly IMemberSignInManager _signInManager; - private readonly IMemberManager _memberManager; - private readonly ITwoFactorLoginService _twoFactorLoginService; + _signInManager = signInManager; + _memberManager = memberManager; + _twoFactorLoginService = twoFactorLoginService; + } + [Obsolete("Use ctor with all params")] + public UmbLoginController( + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IMemberSignInManager signInManager) + : this( + umbracoContextAccessor, + databaseFactory, + services, + appCaches, + profilingLogger, + publishedUrlProvider, + signInManager, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) + { + } - [ActivatorUtilitiesConstructor] - public UmbLoginController( - IUmbracoContextAccessor umbracoContextAccessor, - IUmbracoDatabaseFactory databaseFactory, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger profilingLogger, - IPublishedUrlProvider publishedUrlProvider, - IMemberSignInManager signInManager, - IMemberManager memberManager, - ITwoFactorLoginService twoFactorLoginService) - : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) + [HttpPost] + [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] + public async Task HandleLogin([Bind(Prefix = "loginModel")] LoginModel model) + { + if (ModelState.IsValid == false) { - _signInManager = signInManager; - _memberManager = memberManager; - _twoFactorLoginService = twoFactorLoginService; - } - - [Obsolete("Use ctor with all params")] - public UmbLoginController( - IUmbracoContextAccessor umbracoContextAccessor, - IUmbracoDatabaseFactory databaseFactory, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger profilingLogger, - IPublishedUrlProvider publishedUrlProvider, - IMemberSignInManager signInManager) - : this(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider, signInManager, - StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) - { - - } - - [HttpPost] - [ValidateAntiForgeryToken] - [ValidateUmbracoFormRouteString] - public async Task HandleLogin([Bind(Prefix = "loginModel")]LoginModel model) - { - if (ModelState.IsValid == false) - { - return CurrentUmbracoPage(); - } - - MergeRouteValuesToModel(model); - - // Sign the user in with username/password, this also gives a chance for developers to - // custom verify the credentials and auto-link user accounts with a custom IBackOfficePasswordChecker - SignInResult result = await _signInManager.PasswordSignInAsync( - model.Username, model.Password, isPersistent: model.RememberMe, lockoutOnFailure: true); - - if (result.Succeeded) - { - TempData["LoginSuccess"] = true; - - // If there is a specified path to redirect to then use it. - if (model.RedirectUrl.IsNullOrWhiteSpace() == false) - { - // Validate the redirect URL. - // If it's not a local URL we'll redirect to the root of the current site. - return Redirect(Url.IsLocalUrl(model.RedirectUrl) - ? model.RedirectUrl - : CurrentPage!.AncestorOrSelf(1)!.Url(PublishedUrlProvider)); - } - - // Redirect to current URL by default. - // This is different from the current 'page' because when using Public Access the current page - // will be the login page, but the URL will be on the requested page so that's where we need - // to redirect too. - return RedirectToCurrentUmbracoUrl(); - } - - if (result.RequiresTwoFactor) - { - MemberIdentityUser attemptedUser = await _memberManager.FindByNameAsync(model.Username); - if (attemptedUser == null) - { - return new ValidationErrorResult( - $"No local member found for username {model.Username}"); - } - - var providerNames = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(attemptedUser.Key); - ViewData.SetTwoFactorProviderNames(providerNames); - } - else if (result.IsLockedOut) - { - ModelState.AddModelError("loginModel", "Member is locked out"); - } - else if (result.IsNotAllowed) - { - ModelState.AddModelError("loginModel", "Member is not allowed"); - } - else - { - ModelState.AddModelError("loginModel", "Invalid username or password"); - } return CurrentUmbracoPage(); } - /// - /// We pass in values via encrypted route values so they cannot be tampered with and merge them into the model for use - /// - /// - private void MergeRouteValuesToModel(LoginModel model) + MergeRouteValuesToModel(model); + + // Sign the user in with username/password, this also gives a chance for developers to + // custom verify the credentials and auto-link user accounts with a custom IBackOfficePasswordChecker + SignInResult result = await _signInManager.PasswordSignInAsync( + model.Username, model.Password, model.RememberMe, true); + + if (result.Succeeded) { - if (RouteData.Values.TryGetValue(nameof(LoginModel.RedirectUrl), out var redirectUrl) && redirectUrl != null) + TempData["LoginSuccess"] = true; + + // If there is a specified path to redirect to then use it. + if (model.RedirectUrl.IsNullOrWhiteSpace() == false) { - model.RedirectUrl = redirectUrl.ToString(); + // Validate the redirect URL. + // If it's not a local URL we'll redirect to the root of the current site. + return Redirect(Url.IsLocalUrl(model.RedirectUrl) + ? model.RedirectUrl + : CurrentPage!.AncestorOrSelf(1)!.Url(PublishedUrlProvider)); } + + // Redirect to current URL by default. + // This is different from the current 'page' because when using Public Access the current page + // will be the login page, but the URL will be on the requested page so that's where we need + // to redirect too. + return RedirectToCurrentUmbracoUrl(); } + if (result.RequiresTwoFactor) + { + MemberIdentityUser attemptedUser = await _memberManager.FindByNameAsync(model.Username); + if (attemptedUser == null!) + { + return new ValidationErrorResult( + $"No local member found for username {model.Username}"); + } + IEnumerable providerNames = + await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(attemptedUser.Key); + ViewData.SetTwoFactorProviderNames(providerNames); + } + else if (result.IsLockedOut) + { + ModelState.AddModelError("loginModel", "Member is locked out"); + } + else if (result.IsNotAllowed) + { + ModelState.AddModelError("loginModel", "Member is not allowed"); + } + else + { + ModelState.AddModelError("loginModel", "Invalid username or password"); + } + + return CurrentUmbracoPage(); + } + + /// + /// We pass in values via encrypted route values so they cannot be tampered with and merge them into the model for use + /// + /// + private void MergeRouteValuesToModel(LoginModel model) + { + if (RouteData.Values.TryGetValue(nameof(LoginModel.RedirectUrl), out var redirectUrl) && redirectUrl != null) + { + model.RedirectUrl = redirectUrl.ToString(); + } } } diff --git a/src/Umbraco.Web.Website/Controllers/UmbLoginStatusController.cs b/src/Umbraco.Web.Website/Controllers/UmbLoginStatusController.cs index f6e560366c..d57a9345af 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbLoginStatusController.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbLoginStatusController.cs @@ -1,4 +1,3 @@ -using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Logging; @@ -11,51 +10,50 @@ using Umbraco.Cms.Web.Common.Models; using Umbraco.Cms.Web.Common.Security; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Website.Controllers +namespace Umbraco.Cms.Web.Website.Controllers; + +[UmbracoMemberAuthorize] +public class UmbLoginStatusController : SurfaceController { - [UmbracoMemberAuthorize] - public class UmbLoginStatusController : SurfaceController + private readonly IMemberSignInManager _signInManager; + + public UmbLoginStatusController( + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IMemberSignInManager signInManager) + : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) + => _signInManager = signInManager; + + [HttpPost] + [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] + public async Task HandleLogout([Bind(Prefix = "logoutModel")] PostRedirectModel model) { - private readonly IMemberSignInManager _signInManager; - - public UmbLoginStatusController( - IUmbracoContextAccessor umbracoContextAccessor, - IUmbracoDatabaseFactory databaseFactory, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger profilingLogger, - IPublishedUrlProvider publishedUrlProvider, - IMemberSignInManager signInManager) - : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) - => _signInManager = signInManager; - - [HttpPost] - [ValidateAntiForgeryToken] - [ValidateUmbracoFormRouteString] - public async Task HandleLogout([Bind(Prefix = "logoutModel")]PostRedirectModel model) + if (ModelState.IsValid == false) { - if (ModelState.IsValid == false) - { - return CurrentUmbracoPage(); - } - - var isLoggedIn = HttpContext.User?.Identity?.IsAuthenticated ?? false; - - if (isLoggedIn) - { - await _signInManager.SignOutAsync(); - } - - TempData["LogoutSuccess"] = true; - - // If there is a specified path to redirect to then use it. - if (model.RedirectUrl.IsNullOrWhiteSpace() == false) - { - return Redirect(model.RedirectUrl!); - } - - // Redirect to current page by default. - return RedirectToCurrentUmbracoPage(); + return CurrentUmbracoPage(); } + + var isLoggedIn = HttpContext.User.Identity?.IsAuthenticated ?? false; + + if (isLoggedIn) + { + await _signInManager.SignOutAsync(); + } + + TempData["LogoutSuccess"] = true; + + // If there is a specified path to redirect to then use it. + if (model.RedirectUrl.IsNullOrWhiteSpace() == false) + { + return Redirect(model.RedirectUrl!); + } + + // Redirect to current page by default. + return RedirectToCurrentUmbracoPage(); } } diff --git a/src/Umbraco.Web.Website/Controllers/UmbProfileController.cs b/src/Umbraco.Web.Website/Controllers/UmbProfileController.cs index a1e832f10c..41286f7dba 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbProfileController.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbProfileController.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core.Cache; @@ -16,136 +13,133 @@ using Umbraco.Cms.Web.Common.Filters; using Umbraco.Cms.Web.Website.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Website.Controllers -{ - [UmbracoMemberAuthorize] - public class UmbProfileController : SurfaceController - { - private readonly IMemberManager _memberManager; - private readonly IMemberService _memberService; - private readonly IMemberTypeService _memberTypeService; - private readonly ICoreScopeProvider _scopeProvider; +namespace Umbraco.Cms.Web.Website.Controllers; - public UmbProfileController( - IUmbracoContextAccessor umbracoContextAccessor, - IUmbracoDatabaseFactory databaseFactory, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger profilingLogger, - IPublishedUrlProvider publishedUrlProvider, - IMemberManager memberManager, - IMemberService memberService, - IMemberTypeService memberTypeService, - ICoreScopeProvider scopeProvider) - : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) +[UmbracoMemberAuthorize] +public class UmbProfileController : SurfaceController +{ + private readonly IMemberManager _memberManager; + private readonly IMemberService _memberService; + private readonly IMemberTypeService _memberTypeService; + private readonly ICoreScopeProvider _scopeProvider; + + public UmbProfileController( + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IMemberManager memberManager, + IMemberService memberService, + IMemberTypeService memberTypeService, + ICoreScopeProvider scopeProvider) + : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) + { + _memberManager = memberManager; + _memberService = memberService; + _memberTypeService = memberTypeService; + _scopeProvider = scopeProvider; + } + + [HttpPost] + [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] + public async Task HandleUpdateProfile([Bind(Prefix = "profileModel")] ProfileModel model) + { + if (ModelState.IsValid == false) { - _memberManager = memberManager; - _memberService = memberService; - _memberTypeService = memberTypeService; - _scopeProvider = scopeProvider; + return CurrentUmbracoPage(); } - [HttpPost] - [ValidateAntiForgeryToken] - [ValidateUmbracoFormRouteString] - public async Task HandleUpdateProfile([Bind(Prefix = "profileModel")] ProfileModel model) + MergeRouteValuesToModel(model); + + MemberIdentityUser? currentMember = await _memberManager.GetUserAsync(HttpContext.User); + if (currentMember == null!) { - if (ModelState.IsValid == false) - { - return CurrentUmbracoPage(); - } - - MergeRouteValuesToModel(model); - - MemberIdentityUser currentMember = await _memberManager.GetUserAsync(HttpContext.User); - if (currentMember == null) - { - // this shouldn't happen, we also don't want to return an error so just redirect to where we came from - return RedirectToCurrentUmbracoPage(); - } - - IdentityResult result = await UpdateMemberAsync(model, currentMember); - if (!result.Succeeded) - { - AddErrors(result); - return CurrentUmbracoPage(); - } - - TempData["FormSuccess"] = true; - - // If there is a specified path to redirect to then use it. - if (model.RedirectUrl.IsNullOrWhiteSpace() == false) - { - return Redirect(model.RedirectUrl!); - } - - // Redirect to current page by default. + // this shouldn't happen, we also don't want to return an error so just redirect to where we came from return RedirectToCurrentUmbracoPage(); } - /// - /// We pass in values via encrypted route values so they cannot be tampered with and merge them into the model for use - /// - /// - private void MergeRouteValuesToModel(ProfileModel model) + IdentityResult result = await UpdateMemberAsync(model, currentMember); + if (!result.Succeeded) { - if (RouteData.Values.TryGetValue(nameof(ProfileModel.RedirectUrl), out var redirectUrl) && redirectUrl != null) - { - model.RedirectUrl = redirectUrl.ToString(); - } + AddErrors(result); + return CurrentUmbracoPage(); } - private void AddErrors(IdentityResult result) + TempData["FormSuccess"] = true; + + // If there is a specified path to redirect to then use it. + if (model.RedirectUrl.IsNullOrWhiteSpace() == false) { - foreach (var error in result.Errors) - { - ModelState.AddModelError("profileModel", error.Description); - } + return Redirect(model.RedirectUrl!); } - private async Task UpdateMemberAsync(ProfileModel model, MemberIdentityUser currentMember) + // Redirect to current page by default. + return RedirectToCurrentUmbracoPage(); + } + + /// + /// We pass in values via encrypted route values so they cannot be tampered with and merge them into the model for use + /// + /// + private void MergeRouteValuesToModel(ProfileModel model) + { + if (RouteData.Values.TryGetValue(nameof(ProfileModel.RedirectUrl), out var redirectUrl) && redirectUrl != null) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - - currentMember.Email = model.Email; - currentMember.Name = model.Name; - currentMember.UserName = model.UserName; - currentMember.Comments = model.Comments; - - IdentityResult saveResult = await _memberManager.UpdateAsync(currentMember); - if (!saveResult.Succeeded) - { - return saveResult; - } - - // now we can update the custom properties - // TODO: Ideally we could do this all through our MemberIdentityUser - - IMember? member = _memberService.GetByKey(currentMember.Key); - if (member == null) - { - // should never happen - throw new InvalidOperationException($"Could not find a member with key: {member?.Key}."); - } - - IMemberType? memberType = _memberTypeService.Get(member.ContentTypeId); - - if (model.MemberProperties != null) - { - foreach (MemberPropertyModel property in model.MemberProperties - //ensure the property they are posting exists - .Where(p => memberType?.PropertyTypeExists(p.Alias) ?? false) - .Where(property => member.Properties.Contains(property.Alias)) - //needs to be editable - .Where(p => memberType?.MemberCanEditProperty(p.Alias) ?? false)) - { - member.Properties[property.Alias]?.SetValue(property.Value); - } - } - - _memberService.Save(member); - - return saveResult; + model.RedirectUrl = redirectUrl.ToString(); } } + + private void AddErrors(IdentityResult result) + { + foreach (IdentityError? error in result.Errors) + { + ModelState.AddModelError("profileModel", error.Description); + } + } + + private async Task UpdateMemberAsync(ProfileModel model, MemberIdentityUser currentMember) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + + currentMember.Email = model.Email; + currentMember.Name = model.Name; + currentMember.UserName = model.UserName; + currentMember.Comments = model.Comments; + + IdentityResult saveResult = await _memberManager.UpdateAsync(currentMember); + if (!saveResult.Succeeded) + { + return saveResult; + } + + // now we can update the custom properties + // TODO: Ideally we could do this all through our MemberIdentityUser + IMember? member = _memberService.GetByKey(currentMember.Key); + if (member == null) + { + // should never happen + throw new InvalidOperationException($"Could not find a member with key: {member?.Key}."); + } + + IMemberType? memberType = _memberTypeService.Get(member.ContentTypeId); + + foreach (MemberPropertyModel property in model.MemberProperties + + // ensure the property they are posting exists + .Where(p => memberType?.PropertyTypeExists(p.Alias) ?? false) + .Where(property => member.Properties.Contains(property.Alias)) + + // needs to be editable + .Where(p => memberType?.MemberCanEditProperty(p.Alias) ?? false)) + { + member.Properties[property.Alias]?.SetValue(property.Value); + } + + _memberService.Save(member); + + return saveResult; + } } diff --git a/src/Umbraco.Web.Website/Controllers/UmbRegisterController.cs b/src/Umbraco.Web.Website/Controllers/UmbRegisterController.cs index 3b25c62a09..9261b356f8 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbRegisterController.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbRegisterController.cs @@ -1,6 +1,3 @@ -using System; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Core.Cache; @@ -17,149 +14,147 @@ using Umbraco.Cms.Web.Common.Security; using Umbraco.Cms.Web.Website.Models; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Website.Controllers +namespace Umbraco.Cms.Web.Website.Controllers; + +public class UmbRegisterController : SurfaceController { - public class UmbRegisterController : SurfaceController + private readonly IMemberManager _memberManager; + private readonly IMemberService _memberService; + private readonly IMemberSignInManager _memberSignInManager; + private readonly ICoreScopeProvider _scopeProvider; + + public UmbRegisterController( + IMemberManager memberManager, + IMemberService memberService, + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IMemberSignInManager memberSignInManager, + ICoreScopeProvider scopeProvider) + : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) { - private readonly IMemberManager _memberManager; - private readonly IMemberService _memberService; - private readonly IMemberSignInManager _memberSignInManager; - private readonly ICoreScopeProvider _scopeProvider; + _memberManager = memberManager; + _memberService = memberService; + _memberSignInManager = memberSignInManager; + _scopeProvider = scopeProvider; + } - public UmbRegisterController( - IMemberManager memberManager, - IMemberService memberService, - IUmbracoContextAccessor umbracoContextAccessor, - IUmbracoDatabaseFactory databaseFactory, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger profilingLogger, - IPublishedUrlProvider publishedUrlProvider, - IMemberSignInManager memberSignInManager, - ICoreScopeProvider scopeProvider) - : base(umbracoContextAccessor, databaseFactory, services, appCaches, profilingLogger, publishedUrlProvider) + [HttpPost] + [ValidateAntiForgeryToken] + [ValidateUmbracoFormRouteString] + public async Task HandleRegisterMember([Bind(Prefix = "registerModel")] RegisterModel model) + { + if (ModelState.IsValid == false) { - _memberManager = memberManager; - _memberService = memberService; - _memberSignInManager = memberSignInManager; - _scopeProvider = scopeProvider; + return CurrentUmbracoPage(); } - [HttpPost] - [ValidateAntiForgeryToken] - [ValidateUmbracoFormRouteString] - public async Task HandleRegisterMember([Bind(Prefix = "registerModel")]RegisterModel model) + MergeRouteValuesToModel(model); + + IdentityResult result = await RegisterMemberAsync(model); + if (result.Succeeded) { - if (ModelState.IsValid == false) + TempData["FormSuccess"] = true; + + // If there is a specified path to redirect to then use it. + if (model.RedirectUrl.IsNullOrWhiteSpace() == false) { - return CurrentUmbracoPage(); + return Redirect(model.RedirectUrl!); } - MergeRouteValuesToModel(model); - - IdentityResult result = await RegisterMemberAsync(model, true); - if (result.Succeeded) - { - TempData["FormSuccess"] = true; - - // If there is a specified path to redirect to then use it. - if (model.RedirectUrl.IsNullOrWhiteSpace() == false) - { - return Redirect(model.RedirectUrl!); - } - - // Redirect to current page by default. - return RedirectToCurrentUmbracoPage(); - } - else - { - AddErrors(result); - return CurrentUmbracoPage(); - } + // Redirect to current page by default. + return RedirectToCurrentUmbracoPage(); } - /// - /// We pass in values via encrypted route values so they cannot be tampered with and merge them into the model for use - /// - /// - private void MergeRouteValuesToModel(RegisterModel model) + AddErrors(result); + return CurrentUmbracoPage(); + } + + /// + /// We pass in values via encrypted route values so they cannot be tampered with and merge them into the model for use + /// + /// + private void MergeRouteValuesToModel(RegisterModel model) + { + if (RouteData.Values.TryGetValue(nameof(RegisterModel.RedirectUrl), out var redirectUrl) && redirectUrl != null) { - if (RouteData.Values.TryGetValue(nameof(RegisterModel.RedirectUrl), out var redirectUrl) && redirectUrl != null) - { - model.RedirectUrl = redirectUrl.ToString(); - } - - if (RouteData.Values.TryGetValue(nameof(RegisterModel.MemberTypeAlias), out var memberTypeAlias) && memberTypeAlias != null) - { - model.MemberTypeAlias = memberTypeAlias.ToString()!; - } - - if (RouteData.Values.TryGetValue(nameof(RegisterModel.UsernameIsEmail), out var usernameIsEmail) && usernameIsEmail != null) - { - model.UsernameIsEmail = usernameIsEmail.ToString() == "True"; - } + model.RedirectUrl = redirectUrl.ToString(); } - private void AddErrors(IdentityResult result) + if (RouteData.Values.TryGetValue(nameof(RegisterModel.MemberTypeAlias), out var memberTypeAlias) && + memberTypeAlias != null) { - foreach (var error in result.Errors) - { - ModelState.AddModelError("registerModel", error.Description); - } + model.MemberTypeAlias = memberTypeAlias.ToString()!; } - /// - /// Registers a new member. - /// - /// Register member model. - /// Flag for whether to log the member in upon successful registration. - /// Result of registration operation. - private async Task RegisterMemberAsync(RegisterModel model, bool logMemberIn = true) + if (RouteData.Values.TryGetValue(nameof(RegisterModel.UsernameIsEmail), out var usernameIsEmail) && + usernameIsEmail != null) { - using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); - - // U4-10762 Server error with "Register Member" snippet (Cannot save member with empty name) - // If name field is empty, add the email address instead. - if (string.IsNullOrEmpty(model.Name) && string.IsNullOrEmpty(model.Email) == false) - { - model.Name = model.Email; - } - - model.Username = (model.UsernameIsEmail || model.Username == null) ? model.Email : model.Username; - - var identityUser = MemberIdentityUser.CreateNew(model.Username, model.Email, model.MemberTypeAlias, true, model.Name); - IdentityResult identityResult = await _memberManager.CreateAsync( - identityUser, - model.Password); - - if (identityResult.Succeeded) - { - // Update the custom properties - // TODO: See TODO in MembersIdentityUser, Should we support custom member properties for persistence/retrieval? - IMember? member = _memberService.GetByKey(identityUser.Key); - if (member == null) - { - // should never happen - throw new InvalidOperationException($"Could not find a member with key: {member?.Key}."); - } - - if (model.MemberProperties != null) - { - foreach (MemberPropertyModel property in model.MemberProperties.Where(p => p.Value != null) - .Where(property => member.Properties.Contains(property.Alias))) - { - member.Properties[property.Alias]?.SetValue(property.Value); - } - } - _memberService.Save(member); - - if (logMemberIn) - { - await _memberSignInManager.SignInAsync(identityUser, false); - } - } - - return identityResult; + model.UsernameIsEmail = usernameIsEmail.ToString() == "True"; } } + + private void AddErrors(IdentityResult result) + { + foreach (IdentityError? error in result.Errors) + { + ModelState.AddModelError("registerModel", error.Description); + } + } + + /// + /// Registers a new member. + /// + /// Register member model. + /// Flag for whether to log the member in upon successful registration. + /// Result of registration operation. + private async Task RegisterMemberAsync(RegisterModel model, bool logMemberIn = true) + { + using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true); + + // U4-10762 Server error with "Register Member" snippet (Cannot save member with empty name) + // If name field is empty, add the email address instead. + if (string.IsNullOrEmpty(model.Name) && string.IsNullOrEmpty(model.Email) == false) + { + model.Name = model.Email; + } + + model.Username = model.UsernameIsEmail || model.Username == null ? model.Email : model.Username; + + var identityUser = + MemberIdentityUser.CreateNew(model.Username, model.Email, model.MemberTypeAlias, true, model.Name); + IdentityResult identityResult = await _memberManager.CreateAsync( + identityUser, + model.Password); + + if (identityResult.Succeeded) + { + // Update the custom properties + // TODO: See TODO in MembersIdentityUser, Should we support custom member properties for persistence/retrieval? + IMember? member = _memberService.GetByKey(identityUser.Key); + if (member == null) + { + // should never happen + throw new InvalidOperationException($"Could not find a member with key: {member?.Key}."); + } + + foreach (MemberPropertyModel property in model.MemberProperties.Where(p => p.Value != null) + .Where(property => member.Properties.Contains(property.Alias))) + { + member.Properties[property.Alias]?.SetValue(property.Value); + } + + _memberService.Save(member); + + if (logMemberIn) + { + await _memberSignInManager.SignInAsync(identityUser, false); + } + } + + return identityResult; + } } diff --git a/src/Umbraco.Web.Website/Controllers/UmbTwoFactorLoginController.cs b/src/Umbraco.Web.Website/Controllers/UmbTwoFactorLoginController.cs index e1762c40a2..47b4389cf7 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbTwoFactorLoginController.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbTwoFactorLoginController.cs @@ -1,10 +1,4 @@ -using System.Collections.Generic; -using System.Security.Claims; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; @@ -15,146 +9,150 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.Persistence; -using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Filters; using Umbraco.Cms.Web.Common.Security; using Umbraco.Extensions; using SignInResult = Microsoft.AspNetCore.Identity.SignInResult; -namespace Umbraco.Cms.Web.Website.Controllers +namespace Umbraco.Cms.Web.Website.Controllers; + +[UmbracoMemberAuthorize] +public class UmbTwoFactorLoginController : SurfaceController { - [UmbracoMemberAuthorize] - public class UmbTwoFactorLoginController : SurfaceController + private readonly ILogger _logger; + private readonly IMemberManager _memberManager; + private readonly IMemberSignInManager _memberSignInManager; + private readonly ITwoFactorLoginService _twoFactorLoginService; + + public UmbTwoFactorLoginController( + ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoDatabaseFactory databaseFactory, + ServiceContext services, + AppCaches appCaches, + IProfilingLogger profilingLogger, + IPublishedUrlProvider publishedUrlProvider, + IMemberSignInManager memberSignInManager, + IMemberManager memberManager, + ITwoFactorLoginService twoFactorLoginService) + : base( + umbracoContextAccessor, + databaseFactory, + services, + appCaches, + profilingLogger, + publishedUrlProvider) { - private readonly IMemberManager _memberManager; - private readonly ITwoFactorLoginService _twoFactorLoginService; - private readonly ILogger _logger; - private readonly IMemberSignInManager _memberSignInManager; + _logger = logger; + _memberSignInManager = memberSignInManager; + _memberManager = memberManager; + _twoFactorLoginService = twoFactorLoginService; + } - public UmbTwoFactorLoginController( - ILogger logger, - IUmbracoContextAccessor umbracoContextAccessor, - IUmbracoDatabaseFactory databaseFactory, - ServiceContext services, - AppCaches appCaches, - IProfilingLogger profilingLogger, - IPublishedUrlProvider publishedUrlProvider, - IMemberSignInManager memberSignInManager, - IMemberManager memberManager, - ITwoFactorLoginService twoFactorLoginService) - : base( - umbracoContextAccessor, - databaseFactory, - services, - appCaches, - profilingLogger, - publishedUrlProvider) + /// + /// Used to retrieve the 2FA providers for code submission + /// + /// + [AllowAnonymous] + public async Task>> Get2FAProviders() + { + MemberIdentityUser? user = await _memberSignInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null!) { - _logger = logger; - _memberSignInManager = memberSignInManager; - _memberManager = memberManager; - _twoFactorLoginService = twoFactorLoginService; + _logger.LogWarning("Get2FAProviders :: No verified member found, returning 404"); + return NotFound(); } - /// - /// Used to retrieve the 2FA providers for code submission - /// - /// - [AllowAnonymous] - public async Task>> Get2FAProviders() - { - var user = await _memberSignInManager.GetTwoFactorAuthenticationUserAsync(); - if (user == null) - { - _logger.LogWarning("Get2FAProviders :: No verified member found, returning 404"); - return NotFound(); - } + IList userFactors = await _memberManager.GetValidTwoFactorProvidersAsync(user); + return new ObjectResult(userFactors); + } - var userFactors = await _memberManager.GetValidTwoFactorProvidersAsync(user); - return new ObjectResult(userFactors); + [AllowAnonymous] + public async Task Verify2FACode(Verify2FACodeModel model, string? returnUrl = null) + { + MemberIdentityUser? user = await _memberSignInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null!) + { + _logger.LogWarning("PostVerify2FACode :: No verified member found, returning 404"); + return NotFound(); } - [AllowAnonymous] - public async Task Verify2FACode(Verify2FACodeModel model, string? returnUrl = null) + if (ModelState.IsValid) { - var user = await _memberSignInManager.GetTwoFactorAuthenticationUserAsync(); - if (user == null) + SignInResult result = await _memberSignInManager.TwoFactorSignInAsync( + model.Provider, + model.Code, + model.IsPersistent, + model.RememberClient); + if (result.Succeeded && returnUrl is not null) { - _logger.LogWarning("PostVerify2FACode :: No verified member found, returning 404"); - return NotFound(); + return RedirectToLocal(returnUrl); } - if (ModelState.IsValid) + if (result.IsLockedOut) { - var result = await _memberSignInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.IsPersistent, model.RememberClient); - if (result.Succeeded && returnUrl is not null) - { - return RedirectToLocal(returnUrl); - } - - if (result.IsLockedOut) - { - ModelState.AddModelError(nameof(Verify2FACodeModel.Code), "Member is locked out"); - } - else if (result.IsNotAllowed) - { - ModelState.AddModelError(nameof(Verify2FACodeModel.Code), "Member is not allowed"); - } - else - { - ModelState.AddModelError(nameof(Verify2FACodeModel.Code), "Invalid code"); - } + ModelState.AddModelError(nameof(Verify2FACodeModel.Code), "Member is locked out"); } + else if (result.IsNotAllowed) + { + ModelState.AddModelError(nameof(Verify2FACodeModel.Code), "Member is not allowed"); + } + else + { + ModelState.AddModelError(nameof(Verify2FACodeModel.Code), "Invalid code"); + } + } + + // We need to set this, to ensure we show the 2fa login page + IEnumerable providerNames = + await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(user.Key); + ViewData.SetTwoFactorProviderNames(providerNames); + return CurrentUmbracoPage(); + } + + [HttpPost] + public async Task ValidateAndSaveSetup( + string providerName, + string secret, + string code, + string? returnUrl = null) + { + MemberIdentityUser? member = await _memberManager.GetCurrentMemberAsync(); + + var isValid = _twoFactorLoginService.ValidateTwoFactorSetup(providerName, secret, code); + + if (member is null || isValid == false) + { + ModelState.AddModelError(nameof(code), "Invalid Code"); - //We need to set this, to ensure we show the 2fa login page - var providerNames = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(user.Key); - ViewData.SetTwoFactorProviderNames(providerNames); return CurrentUmbracoPage(); } - [HttpPost] - public async Task ValidateAndSaveSetup(string providerName, string secret, string code, string? returnUrl = null) + var twoFactorLogin = new TwoFactorLogin { - var member = await _memberManager.GetCurrentMemberAsync(); + Confirmed = true, Secret = secret, UserOrMemberKey = member.Key, ProviderName = providerName, + }; - var isValid = _twoFactorLoginService.ValidateTwoFactorSetup(providerName, secret, code); + await _twoFactorLoginService.SaveAsync(twoFactorLogin); - if (member is null || isValid == false) - { - ModelState.AddModelError(nameof(code), "Invalid Code"); - - return CurrentUmbracoPage(); - } - - var twoFactorLogin = new TwoFactorLogin() - { - Confirmed = true, - Secret = secret, - UserOrMemberKey = member.Key, - ProviderName = providerName, - }; - - await _twoFactorLoginService.SaveAsync(twoFactorLogin); - - return RedirectToLocal(returnUrl); - } - - [HttpPost] - public async Task Disable(string providerName, string? returnUrl = null) - { - var member = await _memberManager.GetCurrentMemberAsync(); - - var success = member is not null && await _twoFactorLoginService.DisableAsync(member.Key, providerName); - - if (!success) - { - return CurrentUmbracoPage(); - } - - return RedirectToLocal(returnUrl); - } - - private IActionResult RedirectToLocal(string? returnUrl) => - Url.IsLocalUrl(returnUrl) ? Redirect(returnUrl) : RedirectToCurrentUmbracoPage(); + return RedirectToLocal(returnUrl); } + + [HttpPost] + public async Task Disable(string providerName, string? returnUrl = null) + { + MemberIdentityUser? member = await _memberManager.GetCurrentMemberAsync(); + + var success = member is not null && await _twoFactorLoginService.DisableAsync(member.Key, providerName); + + if (!success) + { + return CurrentUmbracoPage(); + } + + return RedirectToLocal(returnUrl); + } + + private IActionResult RedirectToLocal(string? returnUrl) => + Url.IsLocalUrl(returnUrl) ? Redirect(returnUrl) : RedirectToCurrentUmbracoPage(); } diff --git a/src/Umbraco.Web.Website/Controllers/UmbracoRenderingDefaultsOptions.cs b/src/Umbraco.Web.Website/Controllers/UmbracoRenderingDefaultsOptions.cs index fb9f02d385..b6d2a1ec16 100644 --- a/src/Umbraco.Web.Website/Controllers/UmbracoRenderingDefaultsOptions.cs +++ b/src/Umbraco.Web.Website/Controllers/UmbracoRenderingDefaultsOptions.cs @@ -1,16 +1,14 @@ -using System; using Umbraco.Cms.Web.Common.Controllers; -namespace Umbraco.Cms.Web.Website.Controllers +namespace Umbraco.Cms.Web.Website.Controllers; + +/// +/// The defaults used for rendering Umbraco front-end pages +/// +public class UmbracoRenderingDefaultsOptions { /// - /// The defaults used for rendering Umbraco front-end pages + /// Gets the default umbraco render controller type /// - public class UmbracoRenderingDefaultsOptions - { - /// - /// Gets the default umbraco render controller type - /// - public Type DefaultControllerType { get; set; } = typeof(RenderController); - } + public Type DefaultControllerType { get; set; } = typeof(RenderController); } diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilder.MemberIdentity.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilder.MemberIdentity.cs index c208d96972..f732fcdd7e 100644 --- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilder.MemberIdentity.cs +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilder.MemberIdentity.cs @@ -1,23 +1,21 @@ -using System; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Web.Website.Security; +namespace Umbraco.Extensions; -namespace Umbraco.Extensions +/// +/// Extension methods for for the Umbraco back office +/// +public static partial class UmbracoBuilderExtensions { /// - /// Extension methods for for the Umbraco back office + /// Adds support for external login providers in Umbraco /// - public static partial class UmbracoBuilderExtensions + public static IUmbracoBuilder AddMemberExternalLogins( + this IUmbracoBuilder umbracoBuilder, + Action builder) { - /// - /// Adds support for external login providers in Umbraco - /// - public static IUmbracoBuilder AddMemberExternalLogins(this IUmbracoBuilder umbracoBuilder, Action builder) - { - builder(new MemberExternalLoginsBuilder(umbracoBuilder.Services)); - return umbracoBuilder; - } - + builder(new MemberExternalLoginsBuilder(umbracoBuilder.Services)); + return umbracoBuilder; } } diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs index e5f30135fb..89d6962b64 100644 --- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs @@ -13,52 +13,52 @@ using Umbraco.Cms.Web.Website.Routing; using Umbraco.Cms.Web.Website.ViewEngines; using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// extensions for umbraco front-end website +/// +public static partial class UmbracoBuilderExtensions { /// - /// extensions for umbraco front-end website + /// Add services for the umbraco front-end website /// - public static partial class UmbracoBuilderExtensions + public static IUmbracoBuilder AddWebsite(this IUmbracoBuilder builder) { - /// - /// Add services for the umbraco front-end website - /// - public static IUmbracoBuilder AddWebsite(this IUmbracoBuilder builder) - { - builder.WithCollectionBuilder()? - .Add(builder.TypeLoader.GetSurfaceControllers()); + builder.WithCollectionBuilder()? + .Add(builder.TypeLoader.GetSurfaceControllers()); - // Configure MVC startup options for custom view locations - builder.Services.ConfigureOptions(); - builder.Services.ConfigureOptions(); + // Configure MVC startup options for custom view locations + builder.Services.ConfigureOptions(); + builder.Services.ConfigureOptions(); - // Wraps all existing view engines in a ProfilerViewEngine - builder.Services.AddTransient, ProfilingViewEngineWrapperMvcViewOptionsSetup>(); + // Wraps all existing view engines in a ProfilerViewEngine + builder.Services + .AddTransient, ProfilingViewEngineWrapperMvcViewOptionsSetup>(); - // TODO figure out if we need more to work on load balanced setups - builder.Services.AddDataProtection(); - builder.Services.AddAntiforgery(); + // TODO figure out if we need more to work on load balanced setups + builder.Services.AddDataProtection(); + builder.Services.AddAntiforgery(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.TryAddEnumerable(Singleton()); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.TryAddEnumerable(Singleton()); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); + builder.Services.AddSingleton(); + builder.Services.AddSingleton(); - builder - .AddDistributedCache() - .AddModelsBuilder(); + builder + .AddDistributedCache() + .AddModelsBuilder(); - builder.AddMembersIdentity(); + builder.AddMembersIdentity(); - return builder; - } + return builder; } } diff --git a/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs b/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs index 5dee7c6273..0f30a1dcd5 100644 --- a/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/HtmlHelperRenderExtensions.cs @@ -1,10 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; using System.Text; using System.Text.Encodings.Web; -using System.Threading.Tasks; using System.Web; using Microsoft.AspNetCore.Antiforgery; using Microsoft.AspNetCore.DataProtection; @@ -28,879 +24,992 @@ using Umbraco.Cms.Web.Common.Security; using Umbraco.Cms.Web.Website.Collections; using Umbraco.Cms.Web.Website.Controllers; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// HtmlHelper extensions for use in templates +/// +public static class HtmlHelperRenderExtensions { + private static T GetRequiredService(IHtmlHelper htmlHelper) + where T : notnull + => GetRequiredService(htmlHelper.ViewContext); + + private static T GetRequiredService(ViewContext viewContext) + where T : notnull + => viewContext.HttpContext.RequestServices.GetRequiredService(); + /// - /// HtmlHelper extensions for use in templates + /// Renders the markup for the profiler /// - public static class HtmlHelperRenderExtensions + public static IHtmlContent RenderProfiler(this IHtmlHelper helper) + => new HtmlString(GetRequiredService(helper).Render()); + + /// + /// Renders a partial view that is found in the specified area + /// + public static IHtmlContent AreaPartial( + this IHtmlHelper helper, + string partial, + string area, + object? model = null, + ViewDataDictionary? viewData = null) { - private static T GetRequiredService(IHtmlHelper htmlHelper) - where T : notnull - => GetRequiredService(htmlHelper.ViewContext); + var originalArea = helper.ViewContext.RouteData.DataTokens["area"]; + helper.ViewContext.RouteData.DataTokens["area"] = area; + IHtmlContent? result = helper.Partial(partial, model, viewData); + helper.ViewContext.RouteData.DataTokens["area"] = originalArea; + return result; + } - private static T GetRequiredService(ViewContext viewContext) - where T : notnull - => viewContext.HttpContext.RequestServices.GetRequiredService(); + /// + /// Will render the preview badge when in preview mode which is not required ever unless the MVC page you are + /// using does not inherit from UmbracoViewPage + /// + /// + /// See: http://issues.umbraco.org/issue/U4-1614 + /// + public static IHtmlContent PreviewBadge( + this IHtmlHelper helper, + IUmbracoContextAccessor umbracoContextAccessor, + IHttpContextAccessor httpContextAccessor, + GlobalSettings globalSettings, + IIOHelper ioHelper, + ContentSettings contentSettings) + { + IHostingEnvironment hostingEnvironment = GetRequiredService(helper); + IUmbracoContext umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - /// - /// Renders the markup for the profiler - /// - public static IHtmlContent RenderProfiler(this IHtmlHelper helper) - => new HtmlString(GetRequiredService(helper).Render()); - - /// - /// Renders a partial view that is found in the specified area - /// - public static IHtmlContent AreaPartial(this IHtmlHelper helper, string partial, string area, object? model = null, ViewDataDictionary? viewData = null) + if (umbracoContext.InPreviewMode) { - var originalArea = helper.ViewContext.RouteData.DataTokens["area"]; - helper.ViewContext.RouteData.DataTokens["area"] = area; - var result = helper.Partial(partial, model, viewData); - helper.ViewContext.RouteData.DataTokens["area"] = originalArea; - return result; + var htmlBadge = + string.Format( + contentSettings.PreviewBadge, + hostingEnvironment.ToAbsolute(globalSettings.UmbracoPath), + WebUtility.UrlEncode(httpContextAccessor.GetRequiredHttpContext().Request.Path), + umbracoContext.PublishedRequest?.PublishedContent?.Id); + return new HtmlString(htmlBadge); } - /// - /// Will render the preview badge when in preview mode which is not required ever unless the MVC page you are - /// using does not inherit from UmbracoViewPage - /// - /// - /// See: http://issues.umbraco.org/issue/U4-1614 - /// - public static IHtmlContent PreviewBadge(this IHtmlHelper helper, IUmbracoContextAccessor umbracoContextAccessor, IHttpContextAccessor httpContextAccessor, GlobalSettings globalSettings, IIOHelper ioHelper, ContentSettings contentSettings) - { - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); + return HtmlString.Empty; + } - if (umbracoContext.InPreviewMode) + public static async Task CachedPartialAsync( + this IHtmlHelper htmlHelper, + string partialViewName, + object model, + TimeSpan cacheTimeout, + bool cacheByPage = false, + bool cacheByMember = false, + ViewDataDictionary? viewData = null, + Func? contextualKeyBuilder = null) + { + var cacheKey = new StringBuilder(partialViewName); + + // let's always cache by the current culture to allow variants to have different cache results + var cultureName = Thread.CurrentThread.CurrentUICulture.Name; + if (!string.IsNullOrEmpty(cultureName)) + { + cacheKey.AppendFormat("{0}-", cultureName); + } + + IUmbracoContextAccessor umbracoContextAccessor = GetRequiredService(htmlHelper); + umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext); + + if (cacheByPage) + { + if (umbracoContext == null) { - var htmlBadge = - string.Format( - contentSettings.PreviewBadge, - ioHelper.ResolveUrl(globalSettings.UmbracoPath), - WebUtility.UrlEncode(httpContextAccessor.GetRequiredHttpContext().Request.Path), - umbracoContext.PublishedRequest?.PublishedContent?.Id); - return new HtmlString(htmlBadge); + throw new InvalidOperationException( + "Cannot cache by page if the UmbracoContext has not been initialized, this parameter can only be used in the context of an Umbraco request"); } + cacheKey.AppendFormat("{0}-", umbracoContext.PublishedRequest?.PublishedContent?.Id ?? 0); + } + + if (cacheByMember) + { + IMemberManager memberManager = + htmlHelper.ViewContext.HttpContext.RequestServices.GetRequiredService(); + MemberIdentityUser? currentMember = await memberManager.GetCurrentMemberAsync(); + cacheKey.AppendFormat("m{0}-", currentMember?.Id ?? "0"); + } + + if (contextualKeyBuilder != null) + { + var contextualKey = contextualKeyBuilder(model, viewData); + cacheKey.AppendFormat("c{0}-", contextualKey); + } + + AppCaches appCaches = GetRequiredService(htmlHelper); + IHostingEnvironment hostingEnvironment = GetRequiredService(htmlHelper); + + return appCaches.CachedPartialView( + hostingEnvironment, + umbracoContext!, + htmlHelper, + partialViewName, + model, + cacheTimeout, + cacheKey.ToString(), + viewData); + } + + // public static IHtmlContent EditorFor(this IHtmlHelper htmlHelper, string templateName = "", string htmlFieldName = "", object additionalViewData = null) + // where T : new() + // { + // var model = new T(); + // htmlHelper.Contextualize(htmlHelper.ViewContext.CopyWithModel(model)); + // + // // + // // var typedHelper = new HtmlHelper(htmlHelper. + // // htmlHelper. + // // , + // // htmlHelper.ViewDataContainer.CopyWithModel(model)); + // + // + // + // return htmlHelper.EditorForModel(x => model, templateName, htmlFieldName, additionalViewData); + // } + + /// + /// A validation summary that lets you pass in a prefix so that the summary only displays for elements + /// containing the prefix. This allows you to have more than on validation summary on a page. + /// + public static IHtmlContent ValidationSummary( + this IHtmlHelper htmlHelper, + string prefix = "", + bool excludePropertyErrors = false, + string message = "", + object? htmlAttributes = null) + { + if (prefix.IsNullOrWhiteSpace()) + { + return htmlHelper.ValidationSummary(excludePropertyErrors, message, htmlAttributes); + } + + IHtmlGenerator htmlGenerator = GetRequiredService(htmlHelper); + + ViewContext viewContext = htmlHelper.ViewContext.Clone(); + + // change the HTML field name + viewContext.ViewData.TemplateInfo.HtmlFieldPrefix = prefix; + + TagBuilder? tagBuilder = htmlGenerator.GenerateValidationSummary( + viewContext, + excludePropertyErrors, + message, + null, + htmlAttributes); + if (tagBuilder == null) + { return HtmlString.Empty; - } - public static async Task CachedPartialAsync( - this IHtmlHelper htmlHelper, - string partialViewName, - object model, - TimeSpan cacheTimeout, - bool cacheByPage = false, - bool cacheByMember = false, - ViewDataDictionary? viewData = null, - Func? contextualKeyBuilder = null) + return tagBuilder; + } + + /// + /// Returns the result of a child action of a strongly typed SurfaceController + /// + /// The + public static IHtmlContent ActionLink(this IHtmlHelper htmlHelper, string actionName) + where T : SurfaceController => htmlHelper.ActionLink(actionName, typeof(T)); + + /// + /// Returns the result of a child action of a SurfaceController + /// + public static IHtmlContent ActionLink(this IHtmlHelper htmlHelper, string actionName, Type surfaceType) + { + if (actionName == null) { - var cacheKey = new StringBuilder(partialViewName); - // let's always cache by the current culture to allow variants to have different cache results - var cultureName = System.Threading.Thread.CurrentThread.CurrentUICulture.Name; - if (!string.IsNullOrEmpty(cultureName)) - { - cacheKey.AppendFormat("{0}-", cultureName); - } - - var umbracoContextAccessor = GetRequiredService(htmlHelper); - umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext); - - if (cacheByPage) - { - if (umbracoContext == null) - { - throw new InvalidOperationException("Cannot cache by page if the UmbracoContext has not been initialized, this parameter can only be used in the context of an Umbraco request"); - } - - cacheKey.AppendFormat("{0}-", umbracoContext.PublishedRequest?.PublishedContent?.Id ?? 0); - } - - if (cacheByMember) - { - var memberManager = htmlHelper.ViewContext.HttpContext.RequestServices.GetRequiredService(); - var currentMember = await memberManager.GetCurrentMemberAsync(); - cacheKey.AppendFormat("m{0}-", currentMember?.Id ?? "0"); - } - - if (contextualKeyBuilder != null) - { - var contextualKey = contextualKeyBuilder(model, viewData); - cacheKey.AppendFormat("c{0}-", contextualKey); - } - - var appCaches = GetRequiredService(htmlHelper); - var hostingEnvironment = GetRequiredService(htmlHelper); - - return appCaches.CachedPartialView(hostingEnvironment, umbracoContext!, htmlHelper, partialViewName, model, cacheTimeout, cacheKey.ToString(), viewData); + throw new ArgumentNullException(nameof(actionName)); } - // public static IHtmlContent EditorFor(this IHtmlHelper htmlHelper, string templateName = "", string htmlFieldName = "", object additionalViewData = null) - // where T : new() - // { - // var model = new T(); - // htmlHelper.Contextualize(htmlHelper.ViewContext.CopyWithModel(model)); - // - // // - // // var typedHelper = new HtmlHelper(htmlHelper. - // // htmlHelper. - // // , - // // htmlHelper.ViewDataContainer.CopyWithModel(model)); - // - // - // - // return htmlHelper.EditorForModel(x => model, templateName, htmlFieldName, additionalViewData); - // } + if (string.IsNullOrWhiteSpace(actionName)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(actionName)); + } + + if (surfaceType == null) + { + throw new ArgumentNullException(nameof(surfaceType)); + } + + SurfaceControllerTypeCollection surfaceControllerTypeCollection = + GetRequiredService(htmlHelper); + Type? surfaceController = surfaceControllerTypeCollection.SingleOrDefault(x => x == surfaceType); + if (surfaceController == null) + { + throw new InvalidOperationException("Could not find the surface controller of type " + + surfaceType.FullName); + } + + var routeVals = new RouteValueDictionary(new { area = string.Empty }); + + PluginControllerMetadata metaData = PluginController.GetMetadata(surfaceController); + if (!metaData.AreaName.IsNullOrWhiteSpace()) + { + // set the area to the plugin area + if (routeVals.ContainsKey("area")) + { + routeVals["area"] = metaData.AreaName; + } + else + { + routeVals.Add("area", metaData.AreaName); + } + } + + return htmlHelper.ActionLink(actionName, metaData.ControllerName, routeVals); + } + + /// + /// Outputs the hidden html input field for Surface Controller route information + /// + /// The type + /// + /// Typically not used directly because BeginUmbracoForm automatically outputs this value when routing + /// for surface controllers. But this could be used in case a form tag is manually created. + /// + public static IHtmlContent SurfaceControllerHiddenInput( + this IHtmlHelper htmlHelper, + string controllerAction, + string area, + object? additionalRouteVals = null) + where TSurface : SurfaceController + { + var inputField = GetSurfaceControllerHiddenInput( + GetRequiredService(htmlHelper), + ControllerExtensions.GetControllerName(), + controllerAction, + area, + additionalRouteVals); + + return new HtmlString(inputField); + } + + private static string GetSurfaceControllerHiddenInput( + IDataProtectionProvider dataProtectionProvider, + string controllerName, + string controllerAction, + string area, + object? additionalRouteVals = null) + { + var encryptedString = EncryptionHelper.CreateEncryptedRouteString( + dataProtectionProvider, + controllerName, + controllerAction, + area, + additionalRouteVals); + + return ""; + } + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline against a locally declared + /// controller. + /// + /// The HTML helper. + /// Name of the action. + /// Name of the controller. + /// The method. + /// the + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + string controllerName, + FormMethod method) + => html.BeginUmbracoForm(action, controllerName, null, new Dictionary(), method); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline against a locally declared controller + /// + public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, string controllerName) + => html.BeginUmbracoForm(action, controllerName, null, new Dictionary()); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline against a locally declared controller + /// + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + string controllerName, + object additionalRouteVals, + FormMethod method) + => html.BeginUmbracoForm(action, controllerName, additionalRouteVals, new Dictionary(), method); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline against a locally declared controller + /// + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + string controllerName, + object additionalRouteVals) + => html.BeginUmbracoForm(action, controllerName, additionalRouteVals, new Dictionary()); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline against a locally declared controller + /// + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + string controllerName, + object additionalRouteVals, + object htmlAttributes, + FormMethod method) => + html.BeginUmbracoForm( + action, + controllerName, + additionalRouteVals, + HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes), + method); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline against a locally declared controller + /// + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + string controllerName, + object additionalRouteVals, + object htmlAttributes) => + html.BeginUmbracoForm( + action, + controllerName, + additionalRouteVals, + HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline against a locally declared controller + /// + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + string controllerName, + object? additionalRouteVals, + IDictionary htmlAttributes, + FormMethod method) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + if (string.IsNullOrWhiteSpace(action)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(action)); + } + + 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)); + } + + return html.BeginUmbracoForm(action, controllerName, string.Empty, additionalRouteVals, htmlAttributes, method); + } + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline against a locally declared controller + /// + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + string controllerName, + object? additionalRouteVals, + IDictionary htmlAttributes) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + if (string.IsNullOrWhiteSpace(action)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(action)); + } + + 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)); + } + + return html.BeginUmbracoForm(action, controllerName, string.Empty, additionalRouteVals, htmlAttributes); + } + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, Type surfaceType, FormMethod method) + => html.BeginUmbracoForm(action, surfaceType, null, new Dictionary(), method); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, Type surfaceType) + => html.BeginUmbracoForm(action, surfaceType, null, new Dictionary()); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + /// The type + public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, FormMethod method) + where T : SurfaceController => html.BeginUmbracoForm(action, typeof(T), method); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + /// The type + public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action) + where T : SurfaceController => html.BeginUmbracoForm(action, typeof(T)); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + Type surfaceType, + object additionalRouteVals, + FormMethod method) => + html.BeginUmbracoForm( + action, + surfaceType, + additionalRouteVals, + new Dictionary(), + method); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + Type surfaceType, + object additionalRouteVals) => + html.BeginUmbracoForm(action, surfaceType, additionalRouteVals, new Dictionary()); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + /// The type + public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, object additionalRouteVals, FormMethod method) + where T : SurfaceController => html.BeginUmbracoForm(action, typeof(T), additionalRouteVals, method); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + /// The type + public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, object additionalRouteVals) + where T : SurfaceController => html.BeginUmbracoForm(action, typeof(T), additionalRouteVals); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + Type surfaceType, + object additionalRouteVals, + object htmlAttributes, + FormMethod method) => + html.BeginUmbracoForm( + action, + surfaceType, + additionalRouteVals, + HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes), + method); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + Type surfaceType, + object additionalRouteVals, + object htmlAttributes) => + html.BeginUmbracoForm( + action, + surfaceType, + additionalRouteVals, + HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + /// The type + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + object additionalRouteVals, + object htmlAttributes, + FormMethod method) + where T : SurfaceController => + html.BeginUmbracoForm(action, typeof(T), additionalRouteVals, htmlAttributes, method); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + object additionalRouteVals, + object htmlAttributes) + where T : SurfaceController => html.BeginUmbracoForm(action, typeof(T), additionalRouteVals, htmlAttributes); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + Type surfaceType, + object? additionalRouteVals, + IDictionary htmlAttributes, + FormMethod method) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + if (string.IsNullOrWhiteSpace(action)) + { + throw new ArgumentException( + "Value can't be empty or consist only of white-space characters.", + nameof(action)); + } + + if (surfaceType == null) + { + throw new ArgumentNullException(nameof(surfaceType)); + } + + SurfaceControllerTypeCollection surfaceControllerTypeCollection = + GetRequiredService(html); + Type? surfaceController = surfaceControllerTypeCollection.SingleOrDefault(x => x == surfaceType); + if (surfaceController == null) + { + throw new InvalidOperationException("Could not find the surface controller of type " + + surfaceType.FullName); + } + + PluginControllerMetadata metaData = PluginController.GetMetadata(surfaceController); + + var area = string.Empty; + if (metaData.AreaName.IsNullOrWhiteSpace() == false) + { + // Set the area to the plugin area + area = metaData.AreaName; + } + + return html.BeginUmbracoForm( + action, + metaData.ControllerName, + area!, + additionalRouteVals, + htmlAttributes, + method); + } + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + Type surfaceType, + object? additionalRouteVals, + IDictionary htmlAttributes) + => html.BeginUmbracoForm(action, surfaceType, additionalRouteVals, htmlAttributes, FormMethod.Post); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + /// The type + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + object additionalRouteVals, + IDictionary htmlAttributes, + FormMethod method) + where T : SurfaceController => + html.BeginUmbracoForm(action, typeof(T), additionalRouteVals, htmlAttributes, method); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + /// The type + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + object additionalRouteVals, + IDictionary htmlAttributes) + where T : SurfaceController => html.BeginUmbracoForm(action, typeof(T), additionalRouteVals, htmlAttributes); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, string controllerName, string area, FormMethod method) + => html.BeginUmbracoForm(action, controllerName, area, null, new Dictionary(), method); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, string controllerName, string area) + => html.BeginUmbracoForm(action, controllerName, area, null, new Dictionary()); + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + string? controllerName, + string area, + object? additionalRouteVals, + IDictionary htmlAttributes, + FormMethod method) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + if (string.IsNullOrEmpty(action)) + { + throw new ArgumentException("Value can't be empty.", nameof(action)); + } + + 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)); + } + + IUmbracoContextAccessor umbracoContextAccessor = GetRequiredService(html); + IUmbracoContext umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); + var formAction = umbracoContext.OriginalRequestUrl.PathAndQuery; + return html.RenderForm(formAction, method, htmlAttributes, controllerName, action, area, additionalRouteVals); + } + + /// + /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin + /// + public static MvcForm BeginUmbracoForm( + this IHtmlHelper html, + string action, + string controllerName, + string area, + object? additionalRouteVals, + IDictionary htmlAttributes) => + html.BeginUmbracoForm( + action, + controllerName, + area, + additionalRouteVals, + htmlAttributes, + FormMethod.Post); + + /// + /// This renders out the form for us + /// + /// + /// This code is pretty much the same as the underlying MVC code that writes out the form + /// + private static MvcForm RenderForm( + this IHtmlHelper htmlHelper, + string formAction, + FormMethod method, + IDictionary htmlAttributes, + string surfaceController, + string surfaceAction, + string area, + object? additionalRouteVals = null) + { + // ensure that the multipart/form-data is added to the HTML attributes + if (htmlAttributes.ContainsKey("enctype") == false) + { + htmlAttributes.Add("enctype", "multipart/form-data"); + } + + var tagBuilder = new TagBuilder("form"); + tagBuilder.MergeAttributes(htmlAttributes); + + // action is implicitly generated, so htmlAttributes take precedence. + tagBuilder.MergeAttribute("action", formAction); + + // method is an explicit parameter, so it takes precedence over the htmlAttributes. + tagBuilder.MergeAttribute("method", HtmlHelper.GetFormMethodString(method), true); + var traditionalJavascriptEnabled = htmlHelper.ViewContext.ClientValidationEnabled; + if (traditionalJavascriptEnabled) + { + // forms must have an ID for client validation + tagBuilder.GenerateId("form" + Guid.NewGuid().ToString("N"), string.Empty); + } + + htmlHelper.ViewContext.Writer.Write(tagBuilder.RenderStartTag()); + + HtmlEncoder htmlEncoder = GetRequiredService(htmlHelper); + + // new UmbracoForm: + var theForm = new UmbracoForm(htmlHelper.ViewContext, htmlEncoder, surfaceController, surfaceAction, area, additionalRouteVals); + + if (traditionalJavascriptEnabled) + { + htmlHelper.ViewContext.FormContext.FormData["FormId"] = tagBuilder.Attributes["id"]; + } + + return theForm; + } + + /// + /// Used for rendering out the Form for BeginUmbracoForm + /// + internal class UmbracoForm : MvcForm + { + private readonly string _surfaceControllerInput; + private readonly ViewContext _viewContext; /// - /// A validation summary that lets you pass in a prefix so that the summary only displays for elements - /// containing the prefix. This allows you to have more than on validation summary on a page. + /// Initializes a new instance of the class. /// - public static IHtmlContent ValidationSummary( - this IHtmlHelper htmlHelper, - string prefix = "", - bool excludePropertyErrors = false, - string message = "", - object? htmlAttributes = null) - { - if (prefix.IsNullOrWhiteSpace()) - { - return htmlHelper.ValidationSummary(excludePropertyErrors, message, htmlAttributes); - } - - IHtmlGenerator htmlGenerator = GetRequiredService(htmlHelper); - - ViewContext viewContext = htmlHelper.ViewContext.Clone(); - //change the HTML field name - viewContext.ViewData.TemplateInfo.HtmlFieldPrefix = prefix; - - var tagBuilder = htmlGenerator.GenerateValidationSummary( - viewContext, - excludePropertyErrors, - message, - headerTag: null, - htmlAttributes: htmlAttributes); - if (tagBuilder == null) - { - return HtmlString.Empty; - } - - return tagBuilder; - } - - /// - /// Returns the result of a child action of a strongly typed SurfaceController - /// - /// The - public static IHtmlContent ActionLink(this IHtmlHelper htmlHelper, string actionName) - where T : SurfaceController => htmlHelper.ActionLink(actionName, typeof(T)); - - /// - /// Returns the result of a child action of a SurfaceController - /// - public static IHtmlContent ActionLink(this IHtmlHelper htmlHelper, string actionName, Type surfaceType) - { - 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 (surfaceType == null) - { - throw new ArgumentNullException(nameof(surfaceType)); - } - - SurfaceControllerTypeCollection surfaceControllerTypeCollection = GetRequiredService(htmlHelper); - Type? surfaceController = surfaceControllerTypeCollection.SingleOrDefault(x => x == surfaceType); - if (surfaceController == null) - { - throw new InvalidOperationException("Could not find the surface controller of type " + surfaceType.FullName); - } - - var routeVals = new RouteValueDictionary(new { area = "" }); - - PluginControllerMetadata metaData = PluginController.GetMetadata(surfaceController); - if (!metaData.AreaName.IsNullOrWhiteSpace()) - { - // set the area to the plugin area - if (routeVals.ContainsKey("area")) - { - routeVals["area"] = metaData.AreaName; - } - else - { - routeVals.Add("area", metaData.AreaName); - } - } - - return htmlHelper.ActionLink(actionName, metaData.ControllerName, routeVals); - } - - /// - /// Outputs the hidden html input field for Surface Controller route information - /// - /// The type - /// - /// Typically not used directly because BeginUmbracoForm automatically outputs this value when routing - /// for surface controllers. But this could be used in case a form tag is manually created. - /// - public static IHtmlContent SurfaceControllerHiddenInput( - this IHtmlHelper htmlHelper, - string controllerAction, - string area, - object? additionalRouteVals = null) - where TSurface : SurfaceController - { - var inputField = GetSurfaceControllerHiddenInput( - GetRequiredService(htmlHelper), - ControllerExtensions.GetControllerName(), - controllerAction, - area, - additionalRouteVals); - - return new HtmlString(inputField); - } - - private static string GetSurfaceControllerHiddenInput( - IDataProtectionProvider dataProtectionProvider, + public UmbracoForm( + ViewContext viewContext, + HtmlEncoder htmlEncoder, string controllerName, string controllerAction, string area, object? additionalRouteVals = null) + : base(viewContext, htmlEncoder) { - var encryptedString = EncryptionHelper.CreateEncryptedRouteString( - dataProtectionProvider, + _viewContext = viewContext; + _surfaceControllerInput = GetSurfaceControllerHiddenInput( + GetRequiredService(viewContext), controllerName, controllerAction, area, additionalRouteVals); - - return ""; } - /// - /// Used for rendering out the Form for BeginUmbracoForm - /// - internal class UmbracoForm : MvcForm + protected override void GenerateEndForm() { - private readonly ViewContext _viewContext; - private readonly string _surfaceControllerInput; + // Always output an anti-forgery token + IAntiforgery antiforgery = _viewContext.HttpContext.RequestServices.GetRequiredService(); + IHtmlContent antiforgeryHtml = antiforgery.GetHtml(_viewContext.HttpContext); + _viewContext.Writer.Write(antiforgeryHtml.ToHtmlString()); - /// - /// Initializes a new instance of the class. - /// - public UmbracoForm( - ViewContext viewContext, - HtmlEncoder htmlEncoder, - string controllerName, - string controllerAction, - string area, - object? additionalRouteVals = null) - : base(viewContext, htmlEncoder) - { - _viewContext = viewContext; - _surfaceControllerInput = GetSurfaceControllerHiddenInput( - GetRequiredService(viewContext), - controllerName, - controllerAction, - area, - additionalRouteVals); - } + // write out the hidden surface form routes + _viewContext.Writer.Write(_surfaceControllerInput); - protected override void GenerateEndForm() - { - // Always output an anti-forgery token - IAntiforgery antiforgery = _viewContext.HttpContext.RequestServices.GetRequiredService(); - IHtmlContent antiforgeryHtml = antiforgery.GetHtml(_viewContext.HttpContext); - _viewContext.Writer.Write(antiforgeryHtml.ToHtmlString()); - - // write out the hidden surface form routes - _viewContext.Writer.Write(_surfaceControllerInput); - - base.GenerateEndForm(); - } + base.GenerateEndForm(); } - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline against a locally declared controller. - /// - /// The HTML helper. - /// Name of the action. - /// Name of the controller. - /// The method. - /// the - public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, string controllerName, FormMethod method) - => html.BeginUmbracoForm(action, controllerName, null, new Dictionary(), method); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline against a locally declared controller - /// - public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, string controllerName) - => html.BeginUmbracoForm(action, controllerName, null, new Dictionary()); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline against a locally declared controller - /// - public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, string controllerName, object additionalRouteVals, FormMethod method) - => html.BeginUmbracoForm(action, controllerName, additionalRouteVals, new Dictionary(), method); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline against a locally declared controller - /// - public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, string controllerName, object additionalRouteVals) - => html.BeginUmbracoForm(action, controllerName, additionalRouteVals, new Dictionary()); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline against a locally declared controller - /// - public static MvcForm BeginUmbracoForm( - this IHtmlHelper html, - string action, - string controllerName, - object additionalRouteVals, - object htmlAttributes, - FormMethod method) => html.BeginUmbracoForm(action, controllerName, additionalRouteVals, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes), method); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline against a locally declared controller - /// - public static MvcForm BeginUmbracoForm( - this IHtmlHelper html, - string action, - string controllerName, - object additionalRouteVals, - object htmlAttributes) => html.BeginUmbracoForm(action, controllerName, additionalRouteVals, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline against a locally declared controller - /// - public static MvcForm BeginUmbracoForm( - this IHtmlHelper html, - string action, - string controllerName, - object? additionalRouteVals, - IDictionary htmlAttributes, - FormMethod method) - { - if (action == null) - { - throw new ArgumentNullException(nameof(action)); - } - - if (string.IsNullOrWhiteSpace(action)) - { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(action)); - } - - 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)); - } - - return html.BeginUmbracoForm(action, controllerName, "", additionalRouteVals, htmlAttributes, method); - } - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline against a locally declared controller - /// - public static MvcForm BeginUmbracoForm( - this IHtmlHelper html, - string action, - string controllerName, - object? additionalRouteVals, - IDictionary htmlAttributes) - { - if (action == null) - { - throw new ArgumentNullException(nameof(action)); - } - - if (string.IsNullOrWhiteSpace(action)) - { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(action)); - } - - 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)); - } - - return html.BeginUmbracoForm(action, controllerName, string.Empty, additionalRouteVals, htmlAttributes); - } - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, Type surfaceType, FormMethod method) - => html.BeginUmbracoForm(action, surfaceType, null, new Dictionary(), method); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, Type surfaceType) - => html.BeginUmbracoForm(action, surfaceType, null, new Dictionary()); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - /// The type - public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, FormMethod method) - where T : SurfaceController => html.BeginUmbracoForm(action, typeof(T), method); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - /// The type - public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action) - where T : SurfaceController => html.BeginUmbracoForm(action, typeof(T)); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - public static MvcForm BeginUmbracoForm( - this IHtmlHelper html, - string action, - Type surfaceType, - object additionalRouteVals, - FormMethod method) => html.BeginUmbracoForm(action, surfaceType, additionalRouteVals, new Dictionary(), method); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - public static MvcForm BeginUmbracoForm( - this IHtmlHelper html, - string action, - Type surfaceType, - object additionalRouteVals) => html.BeginUmbracoForm(action, surfaceType, additionalRouteVals, new Dictionary()); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - /// The type - public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, object additionalRouteVals, FormMethod method) - where T : SurfaceController => html.BeginUmbracoForm(action, typeof(T), additionalRouteVals, method); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - /// The type - public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, object additionalRouteVals) - where T : SurfaceController => html.BeginUmbracoForm(action, typeof(T), additionalRouteVals); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - public static MvcForm BeginUmbracoForm( - this IHtmlHelper html, - string action, - Type surfaceType, - object additionalRouteVals, - object htmlAttributes, - FormMethod method) => html.BeginUmbracoForm(action, surfaceType, additionalRouteVals, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes), method); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - public static MvcForm BeginUmbracoForm( - this IHtmlHelper html, - string action, - Type surfaceType, - object additionalRouteVals, - object htmlAttributes) => html.BeginUmbracoForm(action, surfaceType, additionalRouteVals, HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes)); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - /// The type - public static MvcForm BeginUmbracoForm( - this IHtmlHelper html, - string action, - object additionalRouteVals, - object htmlAttributes, - FormMethod method) - where T : SurfaceController => html.BeginUmbracoForm(action, typeof(T), additionalRouteVals, htmlAttributes, method); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - public static MvcForm BeginUmbracoForm( - this IHtmlHelper html, - string action, - object additionalRouteVals, - object htmlAttributes) - where T : SurfaceController => html.BeginUmbracoForm(action, typeof(T), additionalRouteVals, htmlAttributes); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - public static MvcForm BeginUmbracoForm( - this IHtmlHelper html, - string action, - Type surfaceType, - object? additionalRouteVals, - IDictionary htmlAttributes, - FormMethod method) - { - - if (action == null) - { - throw new ArgumentNullException(nameof(action)); - } - - if (string.IsNullOrWhiteSpace(action)) - { - throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(action)); - } - - if (surfaceType == null) - { - throw new ArgumentNullException(nameof(surfaceType)); - } - - var surfaceControllerTypeCollection = GetRequiredService(html); - var surfaceController = surfaceControllerTypeCollection.SingleOrDefault(x => x == surfaceType); - if (surfaceController == null) - { - throw new InvalidOperationException("Could not find the surface controller of type " + surfaceType.FullName); - } - - var metaData = PluginController.GetMetadata(surfaceController); - - var area = string.Empty; - if (metaData.AreaName.IsNullOrWhiteSpace() == false) - { - // Set the area to the plugin area - area = metaData.AreaName; - } - - return html.BeginUmbracoForm(action, metaData.ControllerName, area!, additionalRouteVals, htmlAttributes, method); - } - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - public static MvcForm BeginUmbracoForm( - this IHtmlHelper html, - string action, - Type surfaceType, - object? additionalRouteVals, - IDictionary htmlAttributes) - => html.BeginUmbracoForm(action, surfaceType, additionalRouteVals, htmlAttributes, FormMethod.Post); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - /// The type - public static MvcForm BeginUmbracoForm( - this IHtmlHelper html, - string action, - object additionalRouteVals, - IDictionary htmlAttributes, - FormMethod method) - where T : SurfaceController => html.BeginUmbracoForm(action, typeof(T), additionalRouteVals, htmlAttributes, method); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - /// The type - public static MvcForm BeginUmbracoForm( - this IHtmlHelper html, - string action, - object additionalRouteVals, - IDictionary htmlAttributes) - where T : SurfaceController => html.BeginUmbracoForm(action, typeof(T), additionalRouteVals, htmlAttributes); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, string controllerName, string area, FormMethod method) - => html.BeginUmbracoForm(action, controllerName, area, null, new Dictionary(), method); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - public static MvcForm BeginUmbracoForm(this IHtmlHelper html, string action, string controllerName, string area) - => html.BeginUmbracoForm(action, controllerName, area, null, new Dictionary()); - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - public static MvcForm BeginUmbracoForm( - this IHtmlHelper html, - string action, - string? controllerName, - string area, - object? additionalRouteVals, - IDictionary htmlAttributes, - FormMethod method) - { - if (action == null) - { - throw new ArgumentNullException(nameof(action)); - } - - if (string.IsNullOrEmpty(action)) - { - throw new ArgumentException("Value can't be empty.", nameof(action)); - } - - 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)); - } - - IUmbracoContextAccessor umbracoContextAccessor = GetRequiredService(html); - var umbracoContext = umbracoContextAccessor.GetRequiredUmbracoContext(); - var formAction = umbracoContext.OriginalRequestUrl.PathAndQuery; - return html.RenderForm(formAction, method, htmlAttributes, controllerName, action, area, additionalRouteVals); - } - - /// - /// Helper method to create a new form to execute in the Umbraco request pipeline to a surface controller plugin - /// - public static MvcForm BeginUmbracoForm( - this IHtmlHelper html, - string action, - string controllerName, - string area, - object? additionalRouteVals, - IDictionary htmlAttributes) => html.BeginUmbracoForm(action, controllerName, area, additionalRouteVals, htmlAttributes, FormMethod.Post); - - /// - /// This renders out the form for us - /// - /// - /// This code is pretty much the same as the underlying MVC code that writes out the form - /// - private static MvcForm RenderForm( - this IHtmlHelper htmlHelper, - string formAction, - FormMethod method, - IDictionary htmlAttributes, - string surfaceController, - string surfaceAction, - string area, - object? additionalRouteVals = null) - { - // ensure that the multipart/form-data is added to the HTML attributes - if (htmlAttributes.ContainsKey("enctype") == false) - { - htmlAttributes.Add("enctype", "multipart/form-data"); - } - - var tagBuilder = new TagBuilder("form"); - tagBuilder.MergeAttributes(htmlAttributes); - - // action is implicitly generated, so htmlAttributes take precedence. - tagBuilder.MergeAttribute("action", formAction); - - // method is an explicit parameter, so it takes precedence over the htmlAttributes. - tagBuilder.MergeAttribute("method", HtmlHelper.GetFormMethodString(method), true); - var traditionalJavascriptEnabled = htmlHelper.ViewContext.ClientValidationEnabled; - if (traditionalJavascriptEnabled) - { - // forms must have an ID for client validation - tagBuilder.GenerateId("form" + Guid.NewGuid().ToString("N"), string.Empty); - } - - htmlHelper.ViewContext.Writer.Write(tagBuilder.RenderStartTag()); - - var htmlEncoder = GetRequiredService(htmlHelper); - - // new UmbracoForm: - var theForm = new UmbracoForm(htmlHelper.ViewContext, htmlEncoder, surfaceController, surfaceAction, area, additionalRouteVals); - - if (traditionalJavascriptEnabled) - { - htmlHelper.ViewContext.FormContext.FormData["FormId"] = tagBuilder.Attributes["id"]; - } - - return theForm; - } - - - #region If - - /// - /// If is true, the HTML encoded will be returned; otherwise, . - /// - /// The HTML helper. - /// If set to true returns ; otherwise, . - /// The value if true. - /// - /// The HTML encoded value. - /// - public static IHtmlContent If(this IHtmlHelper html, bool test, string valueIfTrue) - => If(html, test, valueIfTrue, string.Empty); - - /// - /// If is true, the HTML encoded will be returned; otherwise, . - /// - /// The HTML helper. - /// If set to true returns ; otherwise, . - /// The value if true. - /// The value if false. - /// - /// The HTML encoded value. - /// - public static IHtmlContent If(this IHtmlHelper html, bool test, string valueIfTrue, string valueIfFalse) - => new HtmlString(HttpUtility.HtmlEncode(test ? valueIfTrue : valueIfFalse)); - - #endregion - - #region Strings - - private static readonly HtmlStringUtilities StringUtilities = new HtmlStringUtilities(); - - /// - /// HTML encodes the text and replaces text line breaks with HTML line breaks. - /// - /// The HTML helper. - /// The text. - /// - /// The HTML encoded text with text line breaks replaced with HTML line breaks (<br />). - /// - public static IHtmlContent ReplaceLineBreaks(this IHtmlHelper helper, string text) - => StringUtilities.ReplaceLineBreaks(text); - - /// - /// Generates a hash based on the text string passed in. This method will detect the - /// security requirements (is FIPS enabled) and return an appropriate hash. - /// - /// The - /// The text to create a hash from - /// Hash of the text string - public static string CreateHash(this IHtmlHelper helper, string text) => text.GenerateHash(); - - /// - /// Strips all HTML tags from a given string, all contents of the tags will remain. - /// - public static IHtmlContent StripHtml(this IHtmlHelper helper, IHtmlContent html, params string[] tags) - => helper.StripHtml(html.ToHtmlString(), tags); - - /// - /// Strips all HTML tags from a given string, all contents of the tags will remain. - /// - public static IHtmlContent StripHtml(this IHtmlHelper helper, string html, params string[] tags) - => StringUtilities.StripHtmlTags(html, tags); - - /// - /// Will take the first non-null value in the collection and return the value of it. - /// - public static string Coalesce(this IHtmlHelper helper, params object[] args) - => StringUtilities.Coalesce(args); - - /// - /// Joins any number of int/string/objects into one string - /// - public static string Concatenate(this IHtmlHelper helper, params object[] args) - => StringUtilities.Concatenate(args); - - /// - /// Joins any number of int/string/objects into one string and separates them with the string separator parameter. - /// - public static string Join(this IHtmlHelper helper, string separator, params object[] args) - => StringUtilities.Join(separator, args); - - /// - /// Truncates a string to a given length, can add a ellipsis at the end (...). Method checks for open HTML tags, and makes sure to close them - /// - public static IHtmlContent Truncate(this IHtmlHelper helper, IHtmlContent html, int length) - => helper.Truncate(html.ToHtmlString(), length, true, false); - - /// - /// Truncates a string to a given length, can add a ellipsis at the end (...). Method checks for open HTML tags, and makes sure to close them - /// - public static IHtmlContent Truncate(this IHtmlHelper helper, IHtmlContent html, int length, bool addElipsis) - => helper.Truncate(html.ToHtmlString(), length, addElipsis, false); - - /// - /// Truncates a string to a given length, can add a ellipsis at the end (...). Method checks for open HTML tags, and makes sure to close them - /// - public static IHtmlContent Truncate(this IHtmlHelper helper, IHtmlContent html, int length, bool addElipsis, bool treatTagsAsContent) - => helper.Truncate(html.ToHtmlString(), length, addElipsis, treatTagsAsContent); - - /// - /// Truncates a string to a given length, can add a ellipsis at the end (...). Method checks for open HTML tags, and makes sure to close them - /// - public static IHtmlContent Truncate(this IHtmlHelper helper, string html, int length) - => helper.Truncate(html, length, true, false); - - /// - /// Truncates a string to a given length, can add a ellipsis at the end (...). Method checks for open HTML tags, and makes sure to close them - /// - public static IHtmlContent Truncate(this IHtmlHelper helper, string html, int length, bool addElipsis) - => helper.Truncate(html, length, addElipsis, false); - - /// - /// Truncates a string to a given length, can add a ellipsis at the end (...). Method checks for open HTML tags, and makes sure to close them - /// - public static IHtmlContent Truncate(this IHtmlHelper helper, string html, int length, bool addElipsis, bool treatTagsAsContent) - => StringUtilities.Truncate(html, length, addElipsis, treatTagsAsContent); - - /// - /// Truncates a string to a given amount of words, can add a ellipsis at the end (...). Method checks for open HTML tags, and makes sure to close them - /// - public static IHtmlContent TruncateByWords(this IHtmlHelper helper, string html, int words) - { - int length = StringUtilities.WordsToLength(html, words); - - return helper.Truncate(html, length, true, false); - } - - /// - /// Truncates a string to a given amount of words, can add a ellipsis at the end (...). Method checks for open HTML tags, and makes sure to close them - /// - public static IHtmlContent TruncateByWords(this IHtmlHelper helper, string html, int words, bool addElipsis) - { - int length = StringUtilities.WordsToLength(html, words); - - return helper.Truncate(html, length, addElipsis, false); - } - - /// - /// Truncates a string to a given amount of words, can add a ellipsis at the end (...). Method checks for open HTML tags, and makes sure to close them - /// - public static IHtmlContent TruncateByWords(this IHtmlHelper helper, IHtmlContent html, int words) - { - int length = StringUtilities.WordsToLength(html.ToHtmlString(), words); - - return helper.Truncate(html, length, true, false); - } - - /// - /// Truncates a string to a given amount of words, can add a ellipsis at the end (...). Method checks for open HTML tags, and makes sure to close them - /// - public static IHtmlContent TruncateByWords(this IHtmlHelper helper, IHtmlContent html, int words, bool addElipsis) - { - int length = StringUtilities.WordsToLength(html.ToHtmlString(), words); - - return helper.Truncate(html, length, addElipsis, false); - } - - - #endregion } + + #region If + + /// + /// If is true, the HTML encoded will be returned; + /// otherwise, . + /// + /// The HTML helper. + /// + /// If set to true returns ; otherwise, + /// . + /// + /// The value if true. + /// + /// The HTML encoded value. + /// + public static IHtmlContent If(this IHtmlHelper html, bool test, string valueIfTrue) + => If(html, test, valueIfTrue, string.Empty); + + /// + /// If is true, the HTML encoded will be returned; + /// otherwise, . + /// + /// The HTML helper. + /// + /// If set to true returns ; otherwise, + /// . + /// + /// The value if true. + /// The value if false. + /// + /// The HTML encoded value. + /// + public static IHtmlContent If(this IHtmlHelper html, bool test, string valueIfTrue, string valueIfFalse) + => new HtmlString(HttpUtility.HtmlEncode(test ? valueIfTrue : valueIfFalse)); + + #endregion + + #region Strings + + private static readonly HtmlStringUtilities s_stringUtilities = new(); + + /// + /// HTML encodes the text and replaces text line breaks with HTML line breaks. + /// + /// The HTML helper. + /// The text. + /// + /// The HTML encoded text with text line breaks replaced with HTML line breaks (<br />). + /// + public static IHtmlContent ReplaceLineBreaks(this IHtmlHelper helper, string text) + => s_stringUtilities.ReplaceLineBreaks(text); + + /// + /// Generates a hash based on the text string passed in. This method will detect the + /// security requirements (is FIPS enabled) and return an appropriate hash. + /// + /// The + /// The text to create a hash from + /// Hash of the text string + public static string CreateHash(this IHtmlHelper helper, string text) => text.GenerateHash(); + + /// + /// Strips all HTML tags from a given string, all contents of the tags will remain. + /// + public static IHtmlContent StripHtml(this IHtmlHelper helper, IHtmlContent html, params string[] tags) + => helper.StripHtml(html.ToHtmlString(), tags); + + /// + /// Strips all HTML tags from a given string, all contents of the tags will remain. + /// + public static IHtmlContent StripHtml(this IHtmlHelper helper, string html, params string[] tags) + => s_stringUtilities.StripHtmlTags(html, tags); + + /// + /// Will take the first non-null value in the collection and return the value of it. + /// + public static string Coalesce(this IHtmlHelper helper, params object[] args) + => s_stringUtilities.Coalesce(args); + + /// + /// Joins any number of int/string/objects into one string + /// + public static string Concatenate(this IHtmlHelper helper, params object[] args) + => s_stringUtilities.Concatenate(args); + + /// + /// Joins any number of int/string/objects into one string and separates them with the string separator parameter. + /// + public static string Join(this IHtmlHelper helper, string separator, params object[] args) + => s_stringUtilities.Join(separator, args); + + /// + /// Truncates a string to a given length, can add a ellipsis at the end (...). Method checks for open HTML tags, and + /// makes sure to close them + /// + public static IHtmlContent Truncate(this IHtmlHelper helper, IHtmlContent html, int length) + => helper.Truncate(html.ToHtmlString(), length, true, false); + + /// + /// Truncates a string to a given length, can add a ellipsis at the end (...). Method checks for open HTML tags, and + /// makes sure to close them + /// + public static IHtmlContent Truncate(this IHtmlHelper helper, IHtmlContent html, int length, bool addElipsis) + => helper.Truncate(html.ToHtmlString(), length, addElipsis, false); + + /// + /// Truncates a string to a given length, can add a ellipsis at the end (...). Method checks for open HTML tags, and + /// makes sure to close them + /// + public static IHtmlContent Truncate(this IHtmlHelper helper, IHtmlContent html, int length, bool addElipsis, bool treatTagsAsContent) + => helper.Truncate(html.ToHtmlString(), length, addElipsis, treatTagsAsContent); + + /// + /// Truncates a string to a given length, can add a ellipsis at the end (...). Method checks for open HTML tags, and + /// makes sure to close them + /// + public static IHtmlContent Truncate(this IHtmlHelper helper, string html, int length) + => helper.Truncate(html, length, true, false); + + /// + /// Truncates a string to a given length, can add a ellipsis at the end (...). Method checks for open HTML tags, and + /// makes sure to close them + /// + public static IHtmlContent Truncate(this IHtmlHelper helper, string html, int length, bool addElipsis) + => helper.Truncate(html, length, addElipsis, false); + + /// + /// Truncates a string to a given length, can add a ellipsis at the end (...). Method checks for open HTML tags, and + /// makes sure to close them + /// + public static IHtmlContent Truncate(this IHtmlHelper helper, string html, int length, bool addElipsis, bool treatTagsAsContent) + => s_stringUtilities.Truncate(html, length, addElipsis, treatTagsAsContent); + + /// + /// Truncates a string to a given amount of words, can add a ellipsis at the end (...). Method checks for open HTML + /// tags, and makes sure to close them + /// + public static IHtmlContent TruncateByWords(this IHtmlHelper helper, string html, int words) + { + var length = s_stringUtilities.WordsToLength(html, words); + + return helper.Truncate(html, length, true, false); + } + + /// + /// Truncates a string to a given amount of words, can add a ellipsis at the end (...). Method checks for open HTML + /// tags, and makes sure to close them + /// + public static IHtmlContent TruncateByWords(this IHtmlHelper helper, string html, int words, bool addElipsis) + { + var length = s_stringUtilities.WordsToLength(html, words); + + return helper.Truncate(html, length, addElipsis, false); + } + + /// + /// Truncates a string to a given amount of words, can add a ellipsis at the end (...). Method checks for open HTML + /// tags, and makes sure to close them + /// + public static IHtmlContent TruncateByWords(this IHtmlHelper helper, IHtmlContent html, int words) + { + var length = s_stringUtilities.WordsToLength(html.ToHtmlString(), words); + + return helper.Truncate(html, length, true, false); + } + + /// + /// Truncates a string to a given amount of words, can add a ellipsis at the end (...). Method checks for open HTML + /// tags, and makes sure to close them + /// + public static IHtmlContent TruncateByWords(this IHtmlHelper helper, IHtmlContent html, int words, bool addElipsis) + { + var length = s_stringUtilities.WordsToLength(html.ToHtmlString(), words); + + return helper.Truncate(html, length, addElipsis, false); + } + + #endregion } diff --git a/src/Umbraco.Web.Website/Extensions/LinkGeneratorExtensions.cs b/src/Umbraco.Web.Website/Extensions/LinkGeneratorExtensions.cs index e7d32bb8ee..f046261a79 100644 --- a/src/Umbraco.Web.Website/Extensions/LinkGeneratorExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/LinkGeneratorExtensions.cs @@ -1,51 +1,46 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Linq.Expressions; using System.Reflection; using Microsoft.AspNetCore.Routing; using Umbraco.Cms.Core; using Umbraco.Cms.Web.Website.Controllers; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +public static class LinkGeneratorExtensions { - public static class LinkGeneratorExtensions + /// + /// Return the Url for a Surface Controller + /// + /// The + public static string? GetUmbracoSurfaceUrl( + this LinkGenerator linkGenerator, + Expression> methodSelector) + where T : SurfaceController { - /// - /// Return the Url for a Surface Controller - /// - /// The - public static string? GetUmbracoSurfaceUrl(this LinkGenerator linkGenerator, Expression> methodSelector) - where T : SurfaceController + MethodInfo? method = ExpressionHelper.GetMethodInfo(methodSelector); + IDictionary? methodParams = ExpressionHelper.GetMethodParams(methodSelector); + + if (method == null) { - 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 is null || methodParams.Any() == false) - { - return linkGenerator.GetUmbracoSurfaceUrl(method.Name); - } - - return linkGenerator.GetUmbracoSurfaceUrl(method.Name, methodParams); + throw new MissingMethodException( + $"Could not find the method {methodSelector} on type {typeof(T)} or the result "); } - /// - /// Return the Url for a Surface Controller - /// - /// The - public static string? GetUmbracoSurfaceUrl(this LinkGenerator linkGenerator, string actionName, object? id = null) - where T : SurfaceController => linkGenerator.GetUmbracoControllerUrl( - actionName, - typeof(T), - new Dictionary() - { - ["id"] = id - }); + if (methodParams is null || methodParams.Any() == false) + { + return linkGenerator.GetUmbracoSurfaceUrl(method.Name); + } + + return linkGenerator.GetUmbracoSurfaceUrl(method.Name, methodParams); } + + /// + /// Return the Url for a Surface Controller + /// + /// The + public static string? GetUmbracoSurfaceUrl(this LinkGenerator linkGenerator, string actionName, object? id = null) + where T : SurfaceController => linkGenerator.GetUmbracoControllerUrl( + actionName, + typeof(T), + new Dictionary { ["id"] = id }); } diff --git a/src/Umbraco.Web.Website/Extensions/TypeLoaderExtensions.cs b/src/Umbraco.Web.Website/Extensions/TypeLoaderExtensions.cs index 1964b1c560..f088e53a9a 100644 --- a/src/Umbraco.Web.Website/Extensions/TypeLoaderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/TypeLoaderExtensions.cs @@ -1,27 +1,24 @@ -using System; -using System.Collections.Generic; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Website.Controllers; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods for the class. +/// +// Migrated to .NET Core +public static class TypeLoaderExtensions { /// - /// Provides extension methods for the class. + /// Gets all types implementing . /// - // Migrated to .NET Core - public static class TypeLoaderExtensions - { - /// - /// Gets all types implementing . - /// - internal static IEnumerable GetSurfaceControllers(this TypeLoader typeLoader) - => typeLoader.GetTypes(); + internal static IEnumerable GetSurfaceControllers(this TypeLoader typeLoader) + => typeLoader.GetTypes(); - /// - /// Gets all types implementing . - /// - internal static IEnumerable GetUmbracoApiControllers(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/Extensions/UmbracoApplicationBuilder.Website.cs b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs index 33d42e07c9..549c0844ff 100644 --- a/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs +++ b/src/Umbraco.Web.Website/Extensions/UmbracoApplicationBuilder.Website.cs @@ -1,48 +1,46 @@ -using System; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Web.Common.ApplicationBuilder; using Umbraco.Cms.Web.Common.Middleware; using Umbraco.Cms.Web.Website.Routing; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// extensions for the umbraco front-end website +/// +public static class UmbracoApplicationBuilderExtensions { /// - /// extensions for the umbraco front-end website + /// Adds all required middleware to run the website /// - public static partial class UmbracoApplicationBuilderExtensions + /// + /// + public static IUmbracoApplicationBuilderContext UseWebsite(this IUmbracoApplicationBuilderContext builder) { - /// - /// Adds all required middleware to run the website - /// - /// - /// - public static IUmbracoApplicationBuilderContext UseWebsite(this IUmbracoApplicationBuilderContext builder) + builder.AppBuilder.UseMiddleware(); + return builder; + } + + /// + /// Sets up routes for the front-end umbraco website + /// + public static IUmbracoEndpointBuilderContext UseWebsiteEndpoints(this IUmbracoEndpointBuilderContext builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (!builder.RuntimeState.UmbracoCanBoot()) { - builder.AppBuilder.UseMiddleware(); return builder; } - /// - /// Sets up routes for the front-end umbraco website - /// - public static IUmbracoEndpointBuilderContext UseWebsiteEndpoints(this IUmbracoEndpointBuilderContext builder) - { - if (builder == null) - { - throw new ArgumentNullException(nameof(builder)); - } + FrontEndRoutes surfaceRoutes = builder.ApplicationServices.GetRequiredService(); + surfaceRoutes.CreateRoutes(builder.EndpointRouteBuilder); + builder.EndpointRouteBuilder.MapDynamicControllerRoute("/{**slug}"); - if (!builder.RuntimeState.UmbracoCanBoot()) - { - return builder; - } - - FrontEndRoutes surfaceRoutes = builder.ApplicationServices.GetRequiredService(); - surfaceRoutes.CreateRoutes(builder.EndpointRouteBuilder); - builder.EndpointRouteBuilder.MapDynamicControllerRoute("/{**slug}"); - - return builder; - } + return builder; } } diff --git a/src/Umbraco.Web.Website/Extensions/WebsiteUmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/Extensions/WebsiteUmbracoBuilderExtensions.cs index 65bff41a59..ecc58e6a6c 100644 --- a/src/Umbraco.Web.Website/Extensions/WebsiteUmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/Extensions/WebsiteUmbracoBuilderExtensions.cs @@ -1,84 +1,88 @@ -using System; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Routing; -namespace Umbraco.Extensions +namespace Umbraco.Extensions; + +/// +/// Provides extension methods to the class. +/// +public static class WebsiteUmbracoBuilderExtensions { + #region Uniques + /// - /// Provides extension methods to the class. + /// Sets the content last chance finder. /// - public static class WebsiteUmbracoBuilderExtensions + /// The type of the content last chance finder. + /// The builder. + public static IUmbracoBuilder SetContentLastChanceFinder(this IUmbracoBuilder builder) + where T : class, IContentLastChanceFinder { - #region Uniques - - /// - /// Sets the content last chance finder. - /// - /// The type of the content last chance finder. - /// The builder. - public static IUmbracoBuilder SetContentLastChanceFinder(this IUmbracoBuilder builder) - where T : class, IContentLastChanceFinder - { - builder.Services.AddUnique(); - return builder; - } - - /// - /// Sets the content last chance finder. - /// - /// The builder. - /// A function creating a last chance finder. - public static IUmbracoBuilder SetContentLastChanceFinder(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } - - /// - /// Sets the content last chance finder. - /// - /// The builder. - /// A last chance finder. - public static IUmbracoBuilder SetContentLastChanceFinder(this IUmbracoBuilder builder, IContentLastChanceFinder finder) - { - builder.Services.AddUnique(finder); - return builder; - } - - /// - /// Sets the site domain helper. - /// - /// The type of the site domain helper. - /// - public static IUmbracoBuilder SetSiteDomainHelper(this IUmbracoBuilder builder) - where T : class, ISiteDomainMapper - { - builder.Services.AddUnique(); - return builder; - } - - /// - /// Sets the site domain helper. - /// - /// The builder. - /// A function creating a helper. - public static IUmbracoBuilder SetSiteDomainHelper(this IUmbracoBuilder builder, Func factory) - { - builder.Services.AddUnique(factory); - return builder; - } - - /// - /// Sets the site domain helper. - /// - /// The builder. - /// A helper. - public static IUmbracoBuilder SetSiteDomainHelper(this IUmbracoBuilder builder, ISiteDomainMapper helper) - { - builder.Services.AddUnique(helper); - return builder; - } - - #endregion + builder.Services.AddUnique(); + return builder; } + + /// + /// Sets the content last chance finder. + /// + /// The builder. + /// A function creating a last chance finder. + public static IUmbracoBuilder SetContentLastChanceFinder( + this IUmbracoBuilder builder, + Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } + + /// + /// Sets the content last chance finder. + /// + /// The builder. + /// A last chance finder. + public static IUmbracoBuilder SetContentLastChanceFinder( + this IUmbracoBuilder builder, + IContentLastChanceFinder finder) + { + builder.Services.AddUnique(finder); + return builder; + } + + /// + /// Sets the site domain helper. + /// + /// The type of the site domain helper. + /// + public static IUmbracoBuilder SetSiteDomainHelper(this IUmbracoBuilder builder) + where T : class, ISiteDomainMapper + { + builder.Services.AddUnique(); + return builder; + } + + /// + /// Sets the site domain helper. + /// + /// The builder. + /// A function creating a helper. + public static IUmbracoBuilder SetSiteDomainHelper( + this IUmbracoBuilder builder, + Func factory) + { + builder.Services.AddUnique(factory); + return builder; + } + + /// + /// Sets the site domain helper. + /// + /// The builder. + /// A helper. + public static IUmbracoBuilder SetSiteDomainHelper(this IUmbracoBuilder builder, ISiteDomainMapper helper) + { + builder.Services.AddUnique(helper); + return builder; + } + + #endregion } diff --git a/src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs b/src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs index 3833efb89f..0ad7b9e259 100644 --- a/src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs +++ b/src/Umbraco.Web.Website/Middleware/BasicAuthenticationMiddleware.cs @@ -1,5 +1,4 @@ using System.Net; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; @@ -9,65 +8,62 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Web.BackOffice.Security; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Common.Middleware -{ - /// - /// Provides basic authentication via back-office credentials for public website access if configured for use and the client IP is not allow listed. - /// - public class BasicAuthenticationMiddleware : IMiddleware - { - private readonly IRuntimeState _runtimeState; - private readonly IBasicAuthService _basicAuthService; +namespace Umbraco.Cms.Web.Common.Middleware; - public BasicAuthenticationMiddleware( - IRuntimeState runtimeState, - IBasicAuthService basicAuthService) +/// +/// Provides basic authentication via back-office credentials for public website access if configured for use and the +/// client IP is not allow listed. +/// +public class BasicAuthenticationMiddleware : IMiddleware +{ + private readonly IBasicAuthService _basicAuthService; + private readonly IRuntimeState _runtimeState; + + public BasicAuthenticationMiddleware( + IRuntimeState runtimeState, + IBasicAuthService basicAuthService) + { + _runtimeState = runtimeState; + _basicAuthService = basicAuthService; + } + + /// + public async Task InvokeAsync(HttpContext context, RequestDelegate next) + { + if (_runtimeState.Level < RuntimeLevel.Run || context.Request.IsBackOfficeRequest() || + !_basicAuthService.IsBasicAuthEnabled()) { - _runtimeState = runtimeState; - _basicAuthService = basicAuthService; + await next(context); + return; } - /// - public async Task InvokeAsync(HttpContext context, RequestDelegate next) + IPAddress? clientIPAddress = context.Connection.RemoteIpAddress; + if (clientIPAddress is not null && _basicAuthService.IsIpAllowListed(clientIPAddress)) { - if (_runtimeState.Level < RuntimeLevel.Run || context.Request.IsBackOfficeRequest() || !_basicAuthService.IsBasicAuthEnabled()) - { - await next(context); - return; - } + await next(context); + return; + } - IPAddress? clientIPAddress = context.Connection.RemoteIpAddress; - if (clientIPAddress is not null && _basicAuthService.IsIpAllowListed(clientIPAddress)) - { - await next(context); - return; - } + AuthenticateResult authenticateResult = await context.AuthenticateBackOfficeAsync(); + if (authenticateResult.Succeeded) + { + await next(context); + return; + } - AuthenticateResult authenticateResult = await context.AuthenticateBackOfficeAsync(); - if (authenticateResult.Succeeded) - { - await next(context); - return; - } + if (context.TryGetBasicAuthCredentials(out var username, out var password)) + { + IBackOfficeSignInManager? backOfficeSignInManager = + context.RequestServices.GetService(); - if (context.TryGetBasicAuthCredentials(out var username, out var password)) + if (backOfficeSignInManager is not null && username is not null && password is not null) { - IBackOfficeSignInManager? backOfficeSignInManager = - context.RequestServices.GetService(); + SignInResult signInResult = + await backOfficeSignInManager.PasswordSignInAsync(username, password, false, true); - if (backOfficeSignInManager is not null && username is not null && password is not null) + if (signInResult.Succeeded) { - SignInResult signInResult = - await backOfficeSignInManager.PasswordSignInAsync(username, password, false, true); - - if (signInResult.Succeeded) - { - await next.Invoke(context); - } - else - { - SetUnauthorizedHeader(context); - } + await next.Invoke(context); } else { @@ -76,15 +72,19 @@ namespace Umbraco.Cms.Web.Common.Middleware } else { - // no authorization header SetUnauthorizedHeader(context); } } - - private static void SetUnauthorizedHeader(HttpContext context) + else { - context.Response.StatusCode = 401; - context.Response.Headers.Add("WWW-Authenticate", "Basic realm=\"Umbraco login\""); + // no authorization header + SetUnauthorizedHeader(context); } } + + private static void SetUnauthorizedHeader(HttpContext context) + { + context.Response.StatusCode = 401; + context.Response.Headers.Add("WWW-Authenticate", "Basic realm=\"Umbraco login\""); + } } diff --git a/src/Umbraco.Web.Website/Models/MemberModelBuilderBase.cs b/src/Umbraco.Web.Website/Models/MemberModelBuilderBase.cs index ed91491b31..e75e1c8579 100644 --- a/src/Umbraco.Web.Website/Models/MemberModelBuilderBase.cs +++ b/src/Umbraco.Web.Website/Models/MemberModelBuilderBase.cs @@ -1,86 +1,76 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Web.Website.Models +namespace Umbraco.Cms.Web.Website.Models; + +public abstract class MemberModelBuilderBase { - public abstract class MemberModelBuilderBase + private readonly IShortStringHelper _shortStringHelper; + + public MemberModelBuilderBase(IMemberTypeService memberTypeService, IShortStringHelper shortStringHelper) { - private readonly IShortStringHelper _shortStringHelper; + MemberTypeService = memberTypeService; + _shortStringHelper = shortStringHelper; + } - public MemberModelBuilderBase(IMemberTypeService memberTypeService, IShortStringHelper shortStringHelper) + public IMemberTypeService MemberTypeService { get; } + + protected List GetMemberPropertiesViewModel(IMemberType memberType, IMember? member = null) + { + var viewProperties = new List(); + + var builtIns = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper).Select(x => x.Key).ToArray(); + + IOrderedEnumerable propertyTypes = memberType.PropertyTypes + .Where(x => builtIns.Contains(x.Alias) == false && memberType.MemberCanEditProperty(x.Alias)) + .OrderBy(p => p.SortOrder); + + foreach (IPropertyType prop in propertyTypes) { - MemberTypeService = memberTypeService; - _shortStringHelper = shortStringHelper; - } - - public IMemberTypeService MemberTypeService { get; } - - protected List GetMemberPropertiesViewModel(IMemberType memberType, IMember? member = null) - { - var viewProperties = new List(); - - var builtIns = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper).Select(x => x.Key).ToArray(); - - IOrderedEnumerable propertyTypes = memberType.PropertyTypes - .Where(x => builtIns.Contains(x.Alias) == false && memberType.MemberCanEditProperty(x.Alias)) - .OrderBy(p => p.SortOrder); - - foreach (IPropertyType prop in propertyTypes) + var value = string.Empty; + if (member != null) { - var value = string.Empty; - if (member != null) + IProperty? propValue = member.Properties[prop.Alias]; + if (propValue != null && propValue.GetValue() != null) { - IProperty? propValue = member.Properties[prop.Alias]; - if (propValue != null && propValue.GetValue() != null) - { - value = propValue.GetValue()?.ToString(); - } + value = propValue.GetValue()?.ToString(); } - - var viewProperty = new MemberPropertyModel - { - Alias = prop.Alias, - Name = prop.Name, - Value = value - }; - - // TODO: Perhaps one day we'll ship with our own EditorTempates but for now developers - // can just render their own. - - ////This is a rudimentary check to see what data template we should render - //// if developers want to change the template they can do so dynamically in their views or controllers - //// for a given property. - ////These are the default built-in MVC template types: “Boolean”, “Decimal”, “EmailAddress”, “HiddenInput”, “HTML”, “Object”, “String”, “Text”, and “Url” - //// by default we'll render a text box since we've defined that metadata on the UmbracoProperty.Value property directly. - //if (prop.DataTypeId == new Guid(Constants.PropertyEditors.TrueFalse)) - //{ - // viewProperty.EditorTemplate = "UmbracoBoolean"; - //} - //else - //{ - // switch (prop.DataTypeDatabaseType) - // { - // case DataTypeDatabaseType.Integer: - // viewProperty.EditorTemplate = "Decimal"; - // break; - // case DataTypeDatabaseType.Ntext: - // viewProperty.EditorTemplate = "Text"; - // break; - // case DataTypeDatabaseType.Date: - // case DataTypeDatabaseType.Nvarchar: - // break; - // } - //} - - viewProperties.Add(viewProperty); } - return viewProperties; + var viewProperty = new MemberPropertyModel { Alias = prop.Alias, Name = prop.Name, Value = value }; + + // TODO: Perhaps one day we'll ship with our own EditorTempates but for now developers + // can just render their own. + + ////This is a rudimentary check to see what data template we should render + //// if developers want to change the template they can do so dynamically in their views or controllers + //// for a given property. + ////These are the default built-in MVC template types: “Boolean”, “Decimal”, “EmailAddress”, “HiddenInput”, “HTML”, “Object”, “String”, “Text”, and “Url” + //// by default we'll render a text box since we've defined that metadata on the UmbracoProperty.Value property directly. + // if (prop.DataTypeId == new Guid(Constants.PropertyEditors.TrueFalse)) + // { + // viewProperty.EditorTemplate = "UmbracoBoolean"; + // } + // else + // { + // switch (prop.DataTypeDatabaseType) + // { + // case DataTypeDatabaseType.Integer: + // viewProperty.EditorTemplate = "Decimal"; + // break; + // case DataTypeDatabaseType.Ntext: + // viewProperty.EditorTemplate = "Text"; + // break; + // case DataTypeDatabaseType.Date: + // case DataTypeDatabaseType.Nvarchar: + // break; + // } + // } + viewProperties.Add(viewProperty); } + + return viewProperties; } } diff --git a/src/Umbraco.Web.Website/Models/MemberModelBuilderFactory.cs b/src/Umbraco.Web.Website/Models/MemberModelBuilderFactory.cs index df4725cd26..9271e5bec0 100644 --- a/src/Umbraco.Web.Website/Models/MemberModelBuilderFactory.cs +++ b/src/Umbraco.Web.Website/Models/MemberModelBuilderFactory.cs @@ -2,36 +2,40 @@ using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Web.Website.Models +namespace Umbraco.Cms.Web.Website.Models; + +/// +/// Service to create model builder instances for working with Members on the front-end +/// +public class MemberModelBuilderFactory { - /// - /// Service to create model builder instances for working with Members on the front-end - /// - public class MemberModelBuilderFactory + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IMemberService _memberService; + private readonly IMemberTypeService _memberTypeService; + private readonly IShortStringHelper _shortStringHelper; + + public MemberModelBuilderFactory( + IMemberTypeService memberTypeService, + IMemberService memberService, + IShortStringHelper shortStringHelper, + IHttpContextAccessor httpContextAccessor) { - private readonly IMemberTypeService _memberTypeService; - private readonly IMemberService _memberService; - private readonly IShortStringHelper _shortStringHelper; - private readonly IHttpContextAccessor _httpContextAccessor; - - public MemberModelBuilderFactory(IMemberTypeService memberTypeService, IMemberService memberService, IShortStringHelper shortStringHelper, IHttpContextAccessor httpContextAccessor) - { - _memberTypeService = memberTypeService; - _memberService = memberService; - _shortStringHelper = shortStringHelper; - _httpContextAccessor = httpContextAccessor; - } - - /// - /// Create a - /// - /// - public RegisterModelBuilder CreateRegisterModel() => new RegisterModelBuilder(_memberTypeService, _shortStringHelper); - - /// - /// Create a - /// - /// - public ProfileModelBuilder CreateProfileModel() => new ProfileModelBuilder(_memberTypeService, _memberService, _shortStringHelper, _httpContextAccessor); + _memberTypeService = memberTypeService; + _memberService = memberService; + _shortStringHelper = shortStringHelper; + _httpContextAccessor = httpContextAccessor; } + + /// + /// Create a + /// + /// + public RegisterModelBuilder CreateRegisterModel() => new(_memberTypeService, _shortStringHelper); + + /// + /// Create a + /// + /// + public ProfileModelBuilder CreateProfileModel() => + new(_memberTypeService, _memberService, _shortStringHelper, _httpContextAccessor); } diff --git a/src/Umbraco.Web.Website/Models/NoNodesViewModel.cs b/src/Umbraco.Web.Website/Models/NoNodesViewModel.cs index 0c2cc270f9..e44d0e4e98 100644 --- a/src/Umbraco.Web.Website/Models/NoNodesViewModel.cs +++ b/src/Umbraco.Web.Website/Models/NoNodesViewModel.cs @@ -1,7 +1,6 @@ -namespace Umbraco.Cms.Web.Website.Models +namespace Umbraco.Cms.Web.Website.Models; + +public class NoNodesViewModel { - public class NoNodesViewModel - { - public string? UmbracoPath { get; set; } - } + public string? UmbracoPath { get; set; } } diff --git a/src/Umbraco.Web.Website/Models/ProfileModel.cs b/src/Umbraco.Web.Website/Models/ProfileModel.cs index 85c2eaaebf..573956c472 100644 --- a/src/Umbraco.Web.Website/Models/ProfileModel.cs +++ b/src/Umbraco.Web.Website/Models/ProfileModel.cs @@ -1,61 +1,57 @@ -using System; -using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Web.Common.Models; -namespace Umbraco.Cms.Web.Website.Models +namespace Umbraco.Cms.Web.Website.Models; + +/// +/// A readonly member profile model +/// +public class ProfileModel : PostRedirectModel { + [ReadOnly(true)] + public Guid Key { get; set; } + + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } = null!; + /// - /// A readonly member profile model + /// The member's real name /// - public class ProfileModel : PostRedirectModel - { + public string? Name { get; set; } - [ReadOnly(true)] - public Guid Key { get; set; } + [ReadOnly(true)] + public string UserName { get; set; } = null!; - [Required] - [EmailAddress] - [Display(Name = "Email")] - public string Email { get; set; } = null!; + [ReadOnly(true)] + public string? Comments { get; set; } - /// - /// The member's real name - /// - public string? Name { get; set; } + [ReadOnly(true)] + public bool IsApproved { get; set; } - [ReadOnly(true)] - public string UserName { get; set; } = null!; + [ReadOnly(true)] + public bool IsLockedOut { get; set; } - [ReadOnly(true)] - public string? Comments { get; set; } + [ReadOnly(true)] + public DateTime? LastLockoutDate { get; set; } - [ReadOnly(true)] - public bool IsApproved { get; set; } + [ReadOnly(true)] + public DateTime CreatedDate { get; set; } - [ReadOnly(true)] - public bool IsLockedOut { get; set; } + [ReadOnly(true)] + public DateTime? LastLoginDate { get; set; } - [ReadOnly(true)] - public DateTime? LastLockoutDate { get; set; } + [ReadOnly(true)] + public DateTime? LastPasswordChangedDate { get; set; } - [ReadOnly(true)] - public DateTime CreatedDate { get; set; } - - [ReadOnly(true)] - public DateTime? LastLoginDate { get; set; } - - [ReadOnly(true)] - public DateTime? LastPasswordChangedDate { get; set; } - - /// - /// The list of member properties - /// - /// - /// Adding items to this list on the front-end will not add properties to the member in the database. - /// - public List MemberProperties { get; set; } = new List(); - } + /// + /// The list of member properties + /// + /// + /// Adding items to this list on the front-end will not add properties to the member in the database. + /// + public List MemberProperties { get; set; } = new(); } diff --git a/src/Umbraco.Web.Website/Models/ProfileModelBuilder.cs b/src/Umbraco.Web.Website/Models/ProfileModelBuilder.cs index e9d43e3295..982fb127cd 100644 --- a/src/Umbraco.Web.Website/Models/ProfileModelBuilder.cs +++ b/src/Umbraco.Web.Website/Models/ProfileModelBuilder.cs @@ -1,97 +1,96 @@ -using System; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -using Umbraco.Cms.Web.Common.Security; -namespace Umbraco.Cms.Web.Website.Models +namespace Umbraco.Cms.Web.Website.Models; + +public class ProfileModelBuilder : MemberModelBuilderBase { - public class ProfileModelBuilder : MemberModelBuilderBase + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IMemberService _memberService; + private bool _lookupProperties; + private string? _redirectUrl; + + public ProfileModelBuilder( + IMemberTypeService memberTypeService, + IMemberService memberService, + IShortStringHelper shortStringHelper, + IHttpContextAccessor httpContextAccessor) + : base(memberTypeService, shortStringHelper) { - private readonly IMemberService _memberService; - private readonly IHttpContextAccessor _httpContextAccessor; - private string? _redirectUrl; - private bool _lookupProperties; + _memberService = memberService; + _httpContextAccessor = httpContextAccessor; + } - public ProfileModelBuilder( - IMemberTypeService memberTypeService, - IMemberService memberService, - IShortStringHelper shortStringHelper, - IHttpContextAccessor httpContextAccessor) - : base(memberTypeService, shortStringHelper) + public ProfileModelBuilder WithRedirectUrl(string redirectUrl) + { + _redirectUrl = redirectUrl; + return this; + } + + public ProfileModelBuilder WithCustomProperties(bool lookupProperties) + { + _lookupProperties = lookupProperties; + return this; + } + + public async Task BuildForCurrentMemberAsync() + { + IMemberManager? memberManager = + _httpContextAccessor.HttpContext?.RequestServices.GetRequiredService(); + + if (memberManager == null) { - _memberService = memberService; - _httpContextAccessor = httpContextAccessor; + return null; } - public ProfileModelBuilder WithRedirectUrl(string redirectUrl) + MemberIdentityUser? member = _httpContextAccessor.HttpContext is null + ? null + : await memberManager.GetUserAsync(_httpContextAccessor.HttpContext.User); + + if (member == null) { - _redirectUrl = redirectUrl; - return this; + return null; } - public ProfileModelBuilder WithCustomProperties(bool lookupProperties) + var model = new ProfileModel { - _lookupProperties = lookupProperties; - return this; + Name = member.Name, + Email = member.Email, + UserName = member.UserName, + Comments = member.Comments, + IsApproved = member.IsApproved, + IsLockedOut = member.IsLockedOut, + LastLockoutDate = member.LastLockoutDateUtc?.ToLocalTime(), + CreatedDate = member.CreatedDateUtc.ToLocalTime(), + LastLoginDate = member.LastLoginDateUtc?.ToLocalTime(), + LastPasswordChangedDate = member.LastPasswordChangeDateUtc?.ToLocalTime(), + RedirectUrl = _redirectUrl, + Key = member.Key, + }; + + IMemberType? memberType = member.MemberTypeAlias is null ? null : MemberTypeService.Get(member.MemberTypeAlias); + if (memberType == null) + { + throw new InvalidOperationException($"Could not find a member type with alias: {member.MemberTypeAlias}."); } - public async Task BuildForCurrentMemberAsync() + // TODO: This wouldn't be required if we support exposing custom member properties on the MemberIdentityUser at the ASP.NET Identity level. + IMember? persistedMember = _memberService.GetByKey(member.Key); + if (persistedMember == null) { - IMemberManager? memberManager = _httpContextAccessor.HttpContext?.RequestServices.GetRequiredService(); - - if (memberManager == null) - { - return null; - } - - MemberIdentityUser? member = _httpContextAccessor.HttpContext is null ? null : await memberManager.GetUserAsync(_httpContextAccessor.HttpContext.User); - - if (member == null) - { - return null; - } - - var model = new ProfileModel - { - Name = member.Name, - Email = member.Email, - UserName = member.UserName, - Comments = member.Comments, - IsApproved = member.IsApproved, - IsLockedOut = member.IsLockedOut, - LastLockoutDate = member.LastLockoutDateUtc?.ToLocalTime(), - CreatedDate = member.CreatedDateUtc.ToLocalTime(), - LastLoginDate = member.LastLoginDateUtc?.ToLocalTime(), - LastPasswordChangedDate = member.LastPasswordChangeDateUtc?.ToLocalTime(), - RedirectUrl = _redirectUrl, - Key = member.Key - }; - - IMemberType? memberType = member.MemberTypeAlias is null ? null : MemberTypeService.Get(member.MemberTypeAlias); - if (memberType == null) - { - throw new InvalidOperationException($"Could not find a member type with alias: {member.MemberTypeAlias}."); - } - - // TODO: This wouldn't be required if we support exposing custom member properties on the MemberIdentityUser at the ASP.NET Identity level. - IMember? persistedMember = _memberService.GetByKey(member.Key); - if (persistedMember == null) - { - // should never happen - throw new InvalidOperationException($"Could not find a member with key: {member.Key}."); - } - - if (_lookupProperties) - { - model.MemberProperties = GetMemberPropertiesViewModel(memberType, persistedMember); - } - - return model; + // should never happen + throw new InvalidOperationException($"Could not find a member with key: {member.Key}."); } + + if (_lookupProperties) + { + model.MemberProperties = GetMemberPropertiesViewModel(memberType, persistedMember); + } + + return model; } } diff --git a/src/Umbraco.Web.Website/Models/RegisterModel.cs b/src/Umbraco.Web.Website/Models/RegisterModel.cs index ccdabb54f2..3717ef3e7d 100644 --- a/src/Umbraco.Web.Website/Models/RegisterModel.cs +++ b/src/Umbraco.Web.Website/Models/RegisterModel.cs @@ -1,64 +1,62 @@ -using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Web.Common.Models; +using DataType = System.ComponentModel.DataAnnotations.DataType; -namespace Umbraco.Cms.Web.Website.Models +namespace Umbraco.Cms.Web.Website.Models; + +public class RegisterModel : PostRedirectModel { - - public class RegisterModel : PostRedirectModel + public RegisterModel() { - public RegisterModel() - { - MemberTypeAlias = Constants.Conventions.MemberTypes.DefaultAlias; - UsernameIsEmail = true; - MemberProperties = new List(); - } - - [Required] - [EmailAddress] - [Display(Name = "Email")] - public string Email { get; set; } = null!; - - /// - /// Returns the member properties - /// - public List MemberProperties { get; set; } - - /// - /// The member type alias to use to register the member - /// - [Editable(false)] - public string MemberTypeAlias { get; set; } - - /// - /// The members real name - /// - public string Name { get; set; } = null!; - - /// - /// The members password - /// - [Required] - [StringLength(256)] - [DataType(System.ComponentModel.DataAnnotations.DataType.Password)] - [Display(Name = "Password")] - public string Password { get; set; } = null!; - - [DataType(System.ComponentModel.DataAnnotations.DataType.Password)] - [Display(Name = "Confirm password")] - [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] - public string ConfirmPassword { get; set; } = null!; - - /// - /// The username of the model, if UsernameIsEmail is true then this is ignored. - /// - public string? Username { get; set; } - - /// - /// Flag to determine if the username should be the email address, if true then the Username property is ignored - /// - public bool UsernameIsEmail { get; set; } + MemberTypeAlias = Constants.Conventions.MemberTypes.DefaultAlias; + UsernameIsEmail = true; + MemberProperties = new List(); } + + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } = null!; + + /// + /// Returns the member properties + /// + public List MemberProperties { get; set; } + + /// + /// The member type alias to use to register the member + /// + [Editable(false)] + public string MemberTypeAlias { get; set; } + + /// + /// The members real name + /// + public string Name { get; set; } = null!; + + /// + /// The members password + /// + [Required] + [StringLength(256)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } = null!; + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } = null!; + + /// + /// The username of the model, if UsernameIsEmail is true then this is ignored. + /// + public string? Username { get; set; } + + /// + /// Flag to determine if the username should be the email address, if true then the Username property is ignored + /// + public bool UsernameIsEmail { get; set; } } diff --git a/src/Umbraco.Web.Website/Models/RegisterModelBuilder.cs b/src/Umbraco.Web.Website/Models/RegisterModelBuilder.cs index d1170de163..8ce6ab5595 100644 --- a/src/Umbraco.Web.Website/Models/RegisterModelBuilder.cs +++ b/src/Umbraco.Web.Website/Models/RegisterModelBuilder.cs @@ -1,67 +1,67 @@ -using System; -using System.Linq; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; -namespace Umbraco.Cms.Web.Website.Models +namespace Umbraco.Cms.Web.Website.Models; + +/// +/// Builds a for use on the front-end +/// +public class RegisterModelBuilder : MemberModelBuilderBase { + private bool _lookupProperties; + private string? _memberTypeAlias; + private string? _redirectUrl; + private bool _usernameIsEmail; - /// - /// Builds a for use on the front-end - /// - public class RegisterModelBuilder : MemberModelBuilderBase + public RegisterModelBuilder(IMemberTypeService memberTypeService, IShortStringHelper shortStringHelper) + : base(memberTypeService, shortStringHelper) { - private string? _memberTypeAlias; - private bool _lookupProperties; - private bool _usernameIsEmail; - private string? _redirectUrl; + } - public RegisterModelBuilder(IMemberTypeService memberTypeService, IShortStringHelper shortStringHelper) - : base(memberTypeService, shortStringHelper) + public RegisterModelBuilder WithRedirectUrl(string? redirectUrl) + { + _redirectUrl = redirectUrl; + return this; + } + + public RegisterModelBuilder UsernameIsEmail(bool usernameIsEmail = true) + { + _usernameIsEmail = usernameIsEmail; + return this; + } + + public RegisterModelBuilder WithMemberTypeAlias(string memberTypeAlias) + { + _memberTypeAlias = memberTypeAlias; + return this; + } + + public RegisterModelBuilder WithCustomProperties(bool lookupProperties) + { + _lookupProperties = lookupProperties; + return this; + } + + public RegisterModel Build() + { + var providedOrDefaultMemberTypeAlias = _memberTypeAlias ?? Constants.Conventions.MemberTypes.DefaultAlias; + IMemberType? memberType = MemberTypeService.Get(providedOrDefaultMemberTypeAlias); + if (memberType == null) { + throw new InvalidOperationException( + $"Could not find a member type with alias: {providedOrDefaultMemberTypeAlias}."); } - public RegisterModelBuilder WithRedirectUrl(string redirectUrl) + var model = new RegisterModel { - _redirectUrl = redirectUrl; - return this; - } - - public RegisterModelBuilder UsernameIsEmail(bool usernameIsEmail = true) - { - _usernameIsEmail = usernameIsEmail; - return this; - } - - public RegisterModelBuilder WithMemberTypeAlias(string memberTypeAlias) - { - _memberTypeAlias = memberTypeAlias; - return this; - } - - public RegisterModelBuilder WithCustomProperties(bool lookupProperties) - { - _lookupProperties = lookupProperties; - return this; - } - - public RegisterModel Build() - { - var providedOrDefaultMemberTypeAlias = _memberTypeAlias ?? Core.Constants.Conventions.MemberTypes.DefaultAlias; - IMemberType? memberType = MemberTypeService.Get(providedOrDefaultMemberTypeAlias); - if (memberType == null) - { - throw new InvalidOperationException($"Could not find a member type with alias: {providedOrDefaultMemberTypeAlias}."); - } - - var model = new RegisterModel - { - MemberTypeAlias = providedOrDefaultMemberTypeAlias, - UsernameIsEmail = _usernameIsEmail, - MemberProperties = _lookupProperties ? GetMemberPropertiesViewModel(memberType) : Enumerable.Empty().ToList() - }; - return model; - } + MemberTypeAlias = providedOrDefaultMemberTypeAlias, + UsernameIsEmail = _usernameIsEmail, + MemberProperties = _lookupProperties + ? GetMemberPropertiesViewModel(memberType) + : Enumerable.Empty().ToList(), + }; + return model; } } diff --git a/src/Umbraco.Web.Website/Routing/ControllerActionSearcher.cs b/src/Umbraco.Web.Website/Routing/ControllerActionSearcher.cs index 728d005bd8..38fd5080b8 100644 --- a/src/Umbraco.Web.Website/Routing/ControllerActionSearcher.cs +++ b/src/Umbraco.Web.Website/Routing/ControllerActionSearcher.cs @@ -1,6 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.Infrastructure; @@ -10,100 +7,96 @@ using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Web.Common.Controllers; using static Umbraco.Cms.Core.Constants.Web.Routing; -namespace Umbraco.Cms.Web.Website.Routing +namespace Umbraco.Cms.Web.Website.Routing; + +/// +/// Used to find a controller/action in the current available routes +/// +public class ControllerActionSearcher : IControllerActionSearcher { + private const string DefaultActionName = nameof(RenderController.Index); + private readonly IActionSelector _actionSelector; + private readonly ILogger _logger; + /// - /// Used to find a controller/action in the current available routes + /// Initializes a new instance of the class. /// - public class ControllerActionSearcher : IControllerActionSearcher + public ControllerActionSearcher( + ILogger logger, + IActionSelector actionSelector) { - private readonly ILogger _logger; - private readonly IActionSelector _actionSelector; - private const string DefaultActionName = nameof(RenderController.Index); + _logger = logger; + _actionSelector = actionSelector; + } - /// - /// Initializes a new instance of the class. - /// - public ControllerActionSearcher( - ILogger logger, - IActionSelector actionSelector) + /// + /// Determines if a custom controller can hijack the current route + /// + /// The controller type to find + public ControllerActionDescriptor? Find(HttpContext httpContext, string? controller, string? action) => + Find(httpContext, controller, action, null); + + /// + /// Determines if a custom controller can hijack the current route + /// + /// The controller type to find + public ControllerActionDescriptor? Find(HttpContext httpContext, string? controller, string? action, string? area) + { + IReadOnlyList? candidates = + FindControllerCandidates(httpContext, controller, action, DefaultActionName, area); + + if (candidates?.Count > 0) { - _logger = logger; - _actionSelector = actionSelector; + return candidates[0]; } + return null; + } - /// - /// Determines if a custom controller can hijack the current route - /// - /// The controller type to find - public ControllerActionDescriptor? Find(HttpContext httpContext, string? controller, string? action) => Find(httpContext, controller, action, null); - - /// - /// Determines if a custom controller can hijack the current route - /// - /// The controller type to find - public ControllerActionDescriptor? Find(HttpContext httpContext, string? controller, string? action, string? area) + /// + /// Return a list of controller candidates that match the custom controller and action names + /// + private IReadOnlyList? FindControllerCandidates( + HttpContext httpContext, + string? customControllerName, + string? customActionName, + string? defaultActionName, + string? area = null) + { + // Use aspnetcore's IActionSelector to do the finding since it uses an optimized cache lookup + var routeValues = new RouteValueDictionary { - IReadOnlyList? candidates = FindControllerCandidates(httpContext, controller, action, DefaultActionName, area); + [ControllerToken] = customControllerName, + [ActionToken] = customActionName, // first try to find the custom action + }; - if (candidates?.Count > 0) - { - return candidates[0]; - } - - return null; + if (area != null) + { + routeValues[AreaToken] = area; } + var routeData = new RouteData(routeValues); + var routeContext = new RouteContext(httpContext) { RouteData = routeData }; - /// - /// Return a list of controller candidates that match the custom controller and action names - /// - private IReadOnlyList? FindControllerCandidates( - HttpContext httpContext, - string? customControllerName, - string? customActionName, - string? defaultActionName, - string? area = null) + // try finding candidates for the custom action + var candidates = _actionSelector.SelectCandidates(routeContext)? + .Cast() + .Where(x => TypeHelper.IsTypeAssignableFrom(x.ControllerTypeInfo)) + .ToList(); + + if (candidates?.Count > 0) { - // Use aspnetcore's IActionSelector to do the finding since it uses an optimized cache lookup - var routeValues = new RouteValueDictionary - { - [ControllerToken] = customControllerName, - [ActionToken] = customActionName, // first try to find the custom action - }; - - if (area != null) - { - routeValues[AreaToken] = area; - } - - var routeData = new RouteData(routeValues); - var routeContext = new RouteContext(httpContext) - { - RouteData = routeData - }; - - // try finding candidates for the custom action - var candidates = _actionSelector.SelectCandidates(routeContext)? - .Cast() - .Where(x => TypeHelper.IsTypeAssignableFrom(x.ControllerTypeInfo)) - .ToList(); - - if (candidates?.Count > 0) - { - // return them if found - return candidates; - } - - // now find for the default action since we couldn't find the custom one - routeValues[ActionToken] = defaultActionName; - candidates = _actionSelector.SelectCandidates(routeContext)? - .Cast() - .Where(x => TypeHelper.IsTypeAssignableFrom(x.ControllerTypeInfo)) - .ToList(); - + // return them if found return candidates; } + + // now find for the default action since we couldn't find the custom one + routeValues[ActionToken] = defaultActionName; + candidates = _actionSelector.SelectCandidates(routeContext)? + .Cast() + .Where(x => TypeHelper.IsTypeAssignableFrom(x.ControllerTypeInfo)) + .ToList(); + + return candidates; } } diff --git a/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs b/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs index df25f4b66e..6ebf77727a 100644 --- a/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs +++ b/src/Umbraco.Web.Website/Routing/FrontEndRoutes.cs @@ -1,4 +1,3 @@ -using System; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; @@ -11,89 +10,84 @@ using Umbraco.Cms.Web.Common.Routing; using Umbraco.Cms.Web.Website.Collections; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Website.Routing +namespace Umbraco.Cms.Web.Website.Routing; + +/// +/// Creates routes for surface controllers +/// +public sealed class FrontEndRoutes : IAreaRoutes { + private readonly UmbracoApiControllerTypeCollection _apiControllers; + private readonly IRuntimeState _runtimeState; + private readonly SurfaceControllerTypeCollection _surfaceControllerTypeCollection; + private readonly string _umbracoPathSegment; + /// - /// Creates routes for surface controllers + /// Initializes a new instance of the class. /// - public sealed class FrontEndRoutes : IAreaRoutes + public FrontEndRoutes( + IOptions globalSettings, + IHostingEnvironment hostingEnvironment, + IRuntimeState runtimeState, + SurfaceControllerTypeCollection surfaceControllerTypeCollection, + UmbracoApiControllerTypeCollection apiControllers) { - private readonly GlobalSettings _globalSettings; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IRuntimeState _runtimeState; - private readonly SurfaceControllerTypeCollection _surfaceControllerTypeCollection; - private readonly UmbracoApiControllerTypeCollection _apiControllers; - private readonly string _umbracoPathSegment; + _runtimeState = runtimeState; + _surfaceControllerTypeCollection = surfaceControllerTypeCollection; + _apiControllers = apiControllers; + _umbracoPathSegment = globalSettings.Value.GetUmbracoMvcArea(hostingEnvironment); + } - /// - /// Initializes a new instance of the class. - /// - public FrontEndRoutes( - IOptions globalSettings, - IHostingEnvironment hostingEnvironment, - IRuntimeState runtimeState, - SurfaceControllerTypeCollection surfaceControllerTypeCollection, - UmbracoApiControllerTypeCollection apiControllers) + /// + public void CreateRoutes(IEndpointRouteBuilder endpoints) + { + if (_runtimeState.Level != RuntimeLevel.Run) { - _globalSettings = globalSettings.Value; - _hostingEnvironment = hostingEnvironment; - _runtimeState = runtimeState; - _surfaceControllerTypeCollection = surfaceControllerTypeCollection; - _apiControllers = apiControllers; - _umbracoPathSegment = _globalSettings.GetUmbracoMvcArea(_hostingEnvironment); + return; } - /// - public void CreateRoutes(IEndpointRouteBuilder endpoints) - { - if (_runtimeState.Level != RuntimeLevel.Run) - { - return; - } + AutoRouteSurfaceControllers(endpoints); + AutoRouteFrontEndApiControllers(endpoints); + } - AutoRouteSurfaceControllers(endpoints); - AutoRouteFrontEndApiControllers(endpoints); + /// + /// Auto-routes all front-end surface controllers + /// + private void AutoRouteSurfaceControllers(IEndpointRouteBuilder endpoints) + { + foreach (Type controller in _surfaceControllerTypeCollection) + { + // exclude front-end api controllers + PluginControllerMetadata meta = PluginController.GetMetadata(controller); + + endpoints.MapUmbracoSurfaceRoute( + meta.ControllerType, + _umbracoPathSegment, + meta.AreaName); } + } - /// - /// Auto-routes all front-end surface controllers - /// - private void AutoRouteSurfaceControllers(IEndpointRouteBuilder endpoints) + /// + /// Auto-routes all front-end api controllers + /// + private void AutoRouteFrontEndApiControllers(IEndpointRouteBuilder endpoints) + { + foreach (Type controller in _apiControllers) { - foreach (Type controller in _surfaceControllerTypeCollection) + PluginControllerMetadata meta = PluginController.GetMetadata(controller); + + // exclude back-end api controllers + if (meta.IsBackOffice) { - // exclude front-end api controllers - PluginControllerMetadata meta = PluginController.GetMetadata(controller); - - endpoints.MapUmbracoSurfaceRoute( - meta.ControllerType, - _umbracoPathSegment, - meta.AreaName); + continue; } - } - /// - /// 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, - defaultAction: string.Empty); // no default action (this is what we had before) - } + 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.Website/Routing/IControllerActionSearcher.cs b/src/Umbraco.Web.Website/Routing/IControllerActionSearcher.cs index 7a31384ede..434e39ae02 100644 --- a/src/Umbraco.Web.Website/Routing/IControllerActionSearcher.cs +++ b/src/Umbraco.Web.Website/Routing/IControllerActionSearcher.cs @@ -1,16 +1,12 @@ -using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Controllers; -namespace Umbraco.Cms.Web.Website.Routing +namespace Umbraco.Cms.Web.Website.Routing; + +public interface IControllerActionSearcher { - public interface IControllerActionSearcher - { + ControllerActionDescriptor? Find(HttpContext httpContext, string? controller, string? action); - ControllerActionDescriptor? Find(HttpContext httpContext, string? controller, string? action); - - ControllerActionDescriptor? Find(HttpContext httpContext, string? controller, string? action, string? area) - => Find(httpContext, controller, action); - - } + ControllerActionDescriptor? Find(HttpContext httpContext, string? controller, string? action, string? area) + => Find(httpContext, controller, action); } diff --git a/src/Umbraco.Web.Website/Routing/IPublicAccessRequestHandler.cs b/src/Umbraco.Web.Website/Routing/IPublicAccessRequestHandler.cs index d55461f4a2..63aebf1e60 100644 --- a/src/Umbraco.Web.Website/Routing/IPublicAccessRequestHandler.cs +++ b/src/Umbraco.Web.Website/Routing/IPublicAccessRequestHandler.cs @@ -1,18 +1,18 @@ -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Umbraco.Cms.Web.Common.Routing; -namespace Umbraco.Cms.Web.Website.Routing +namespace Umbraco.Cms.Web.Website.Routing; + +public interface IPublicAccessRequestHandler { - public interface IPublicAccessRequestHandler - { - /// - /// Ensures that access to current node is permitted. - /// - /// - /// The current route values - /// Updated route values if public access changes the rendered content, else the original route values. - /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. - Task RewriteForPublishedContentAccessAsync(HttpContext httpContext, UmbracoRouteValues routeValues); - } + /// + /// Ensures that access to current node is permitted. + /// + /// + /// The current route values + /// Updated route values if public access changes the rendered content, else the original route values. + /// Redirecting to a different site root and/or culture will not pick the new site root nor the new culture. + Task RewriteForPublishedContentAccessAsync( + HttpContext httpContext, + UmbracoRouteValues routeValues); } diff --git a/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs b/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs index e25921bd91..715bd76af8 100644 --- a/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs +++ b/src/Umbraco.Web.Website/Routing/IUmbracoRouteValuesFactory.cs @@ -1,18 +1,16 @@ -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Web.Common.Routing; -namespace Umbraco.Cms.Web.Website.Routing +namespace Umbraco.Cms.Web.Website.Routing; + +/// +/// Used to create +/// +public interface IUmbracoRouteValuesFactory { /// - /// Used to create + /// Creates /// - public interface IUmbracoRouteValuesFactory - { - /// - /// Creates - /// - Task CreateAsync(HttpContext httpContext, IPublishedRequest request); - } + Task CreateAsync(HttpContext httpContext, IPublishedRequest request); } diff --git a/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs b/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs index f4bc0a84af..b99b432942 100644 --- a/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs +++ b/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs @@ -1,7 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; @@ -12,85 +8,84 @@ using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Controllers; using Umbraco.Cms.Web.Common.Routing; -namespace Umbraco.Cms.Web.Website.Routing +namespace Umbraco.Cms.Web.Website.Routing; + +/// +/// Used to handle 404 routes that haven't been handled by the end user +/// +internal class NotFoundSelectorPolicy : MatcherPolicy, IEndpointSelectorPolicy { - /// - /// Used to handle 404 routes that haven't been handled by the end user - /// - internal class NotFoundSelectorPolicy : MatcherPolicy, IEndpointSelectorPolicy + private readonly EndpointDataSource _endpointDataSource; + private readonly Lazy _notFound; + + public NotFoundSelectorPolicy(EndpointDataSource endpointDataSource) { - private readonly Lazy _notFound; - private readonly EndpointDataSource _endpointDataSource; + _notFound = new Lazy(GetNotFoundEndpoint); + _endpointDataSource = endpointDataSource; + } - public NotFoundSelectorPolicy(EndpointDataSource endpointDataSource) - { - _notFound = new Lazy(GetNotFoundEndpoint); - _endpointDataSource = endpointDataSource; - } + public override int Order => 0; - // return the endpoint for the RenderController.Index action. - private Endpoint GetNotFoundEndpoint() + public bool AppliesToEndpoints(IReadOnlyList endpoints) + { + // Don't apply this filter to any endpoint group that is a controller route + // i.e. only dynamic routes. + foreach (Endpoint endpoint in endpoints) { - Endpoint e = _endpointDataSource.Endpoints.First(x => + ControllerAttribute? controller = endpoint.Metadata.GetMetadata(); + if (controller != null) { - // return the endpoint for the RenderController.Index action. - ControllerActionDescriptor? descriptor = x.Metadata?.GetMetadata(); - return descriptor?.ControllerTypeInfo == typeof(RenderController) - && descriptor?.ActionName == nameof(RenderController.Index); - }); - return e; - } - - public override int Order => 0; - - public bool AppliesToEndpoints(IReadOnlyList endpoints) - { - // Don't apply this filter to any endpoint group that is a controller route - // i.e. only dynamic routes. - foreach (Endpoint endpoint in endpoints) - { - ControllerAttribute? controller = endpoint.Metadata?.GetMetadata(); - if (controller != null) - { - return false; - } + return false; } - - // then ensure this is only applied if all endpoints are IDynamicEndpointMetadata - return endpoints.All(x => x.Metadata?.GetMetadata() != null); } - public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) + // then ensure this is only applied if all endpoints are IDynamicEndpointMetadata + return endpoints.All(x => x.Metadata.GetMetadata() != null); + } + + public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) + { + if (AllInvalid(candidates)) { - if (AllInvalid(candidates)) + UmbracoRouteValues? umbracoRouteValues = httpContext.Features.Get(); + if (umbracoRouteValues?.PublishedRequest != null + && !umbracoRouteValues.PublishedRequest.HasPublishedContent() + && umbracoRouteValues.PublishedRequest.ResponseStatusCode == StatusCodes.Status404NotFound) { - UmbracoRouteValues? umbracoRouteValues = httpContext.Features.Get(); - if (umbracoRouteValues?.PublishedRequest != null - && !umbracoRouteValues.PublishedRequest.HasPublishedContent() - && umbracoRouteValues.PublishedRequest.ResponseStatusCode == StatusCodes.Status404NotFound) - { - // not found/404 - httpContext.SetEndpoint(_notFound.Value); - } + // not found/404 + httpContext.SetEndpoint(_notFound.Value); } - - return Task.CompletedTask; } - private static bool AllInvalid(CandidateSet candidates) + return Task.CompletedTask; + } + + // return the endpoint for the RenderController.Index action. + private Endpoint GetNotFoundEndpoint() + { + Endpoint e = _endpointDataSource.Endpoints.First(x => { - for (int i = 0; i < candidates.Count; i++) - { - // We have to check if candidates needs to be ignored here - // So we dont return false when all endpoints are invalid - if (candidates.IsValidCandidate(i) && - candidates[i].Endpoint.Metadata.GetMetadata() is null) - { - return false; - } - } + // return the endpoint for the RenderController.Index action. + ControllerActionDescriptor? descriptor = x.Metadata.GetMetadata(); + return descriptor?.ControllerTypeInfo == typeof(RenderController) + && descriptor.ActionName == nameof(RenderController.Index); + }); + return e; + } - return true; + private static bool AllInvalid(CandidateSet candidates) + { + for (var i = 0; i < candidates.Count; i++) + { + // We have to check if candidates needs to be ignored here + // So we dont return false when all endpoints are invalid + if (candidates.IsValidCandidate(i) && + candidates[i].Endpoint.Metadata.GetMetadata() is null) + { + return false; + } } + + return true; } } diff --git a/src/Umbraco.Web.Website/Routing/PublicAccessRequestHandler.cs b/src/Umbraco.Web.Website/Routing/PublicAccessRequestHandler.cs index 3569f31ad3..230dcaefe5 100644 --- a/src/Umbraco.Web.Website/Routing/PublicAccessRequestHandler.cs +++ b/src/Umbraco.Web.Website/Routing/PublicAccessRequestHandler.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; @@ -14,138 +12,142 @@ using Umbraco.Cms.Core.Web; using Umbraco.Cms.Web.Common.Routing; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Website.Routing +namespace Umbraco.Cms.Web.Website.Routing; + +public class PublicAccessRequestHandler : IPublicAccessRequestHandler { - public class PublicAccessRequestHandler : IPublicAccessRequestHandler + private readonly ILogger _logger; + private readonly IPublicAccessChecker _publicAccessChecker; + private readonly IPublicAccessService _publicAccessService; + private readonly IPublishedRouter _publishedRouter; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + private readonly IUmbracoRouteValuesFactory _umbracoRouteValuesFactory; + + public PublicAccessRequestHandler( + ILogger logger, + IPublicAccessService publicAccessService, + IPublicAccessChecker publicAccessChecker, + IUmbracoContextAccessor umbracoContextAccessor, + IUmbracoRouteValuesFactory umbracoRouteValuesFactory, + IPublishedRouter publishedRouter) { - private readonly ILogger _logger; - private readonly IPublicAccessService _publicAccessService; - private readonly IPublicAccessChecker _publicAccessChecker; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IUmbracoRouteValuesFactory _umbracoRouteValuesFactory; - private readonly IPublishedRouter _publishedRouter; + _logger = logger; + _publicAccessService = publicAccessService; + _publicAccessChecker = publicAccessChecker; + _umbracoContextAccessor = umbracoContextAccessor; + _umbracoRouteValuesFactory = umbracoRouteValuesFactory; + _publishedRouter = publishedRouter; + } - public PublicAccessRequestHandler( - ILogger logger, - IPublicAccessService publicAccessService, - IPublicAccessChecker publicAccessChecker, - IUmbracoContextAccessor umbracoContextAccessor, - IUmbracoRouteValuesFactory umbracoRouteValuesFactory, - IPublishedRouter publishedRouter) + /// + public async Task RewriteForPublishedContentAccessAsync( + HttpContext httpContext, + UmbracoRouteValues routeValues) + { + // because these might loop, we have to have some sort of infinite loop detection + var i = 0; + const int maxLoop = 8; + PublicAccessStatus publicAccessStatus; + do { - _logger = logger; - _publicAccessService = publicAccessService; - _publicAccessChecker = publicAccessChecker; - _umbracoContextAccessor = umbracoContextAccessor; - _umbracoRouteValuesFactory = umbracoRouteValuesFactory; - _publishedRouter = publishedRouter; - } + _logger.LogDebug(nameof(RewriteForPublishedContentAccessAsync) + ": Loop {LoopCounter}", i); - /// - public async Task RewriteForPublishedContentAccessAsync(HttpContext httpContext, UmbracoRouteValues routeValues) - { - // because these might loop, we have to have some sort of infinite loop detection - int i = 0; - const int maxLoop = 8; - PublicAccessStatus publicAccessStatus = PublicAccessStatus.AccessAccepted; - do + IPublishedContent? publishedContent = routeValues.PublishedRequest?.PublishedContent; + if (publishedContent == null) { - _logger.LogDebug(nameof(RewriteForPublishedContentAccessAsync) + ": Loop {LoopCounter}", i); - - - IPublishedContent? publishedContent = routeValues.PublishedRequest?.PublishedContent; - if (publishedContent == null) - { - return routeValues; - } - - var path = publishedContent.Path; - - Attempt publicAccessAttempt = _publicAccessService.IsProtected(path); - - if (publicAccessAttempt.Success) - { - _logger.LogDebug("EnsurePublishedContentAccess: Page is protected, check for access"); - - // manually authenticate the request - AuthenticateResult authResult = await httpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme); - if (authResult.Succeeded) - { - // set the user to the auth result. we need to do this here because this occurs - // before the authentication middleware. - // NOTE: It would be possible to just pass the authResult to the HasMemberAccessToContentAsync method - // instead of relying directly on the user assigned to the http context, and then the auth middleware - // will run anyways and assign the user. Perhaps that is a little cleaner, but would require more code - // changes right now, and really it's not any different in the end result. - httpContext.SetPrincipalForRequest(authResult.Principal); - } - - publicAccessStatus = await _publicAccessChecker.HasMemberAccessToContentAsync(publishedContent.Id); - switch (publicAccessStatus) - { - case PublicAccessStatus.NotLoggedIn: - _logger.LogDebug("EnsurePublishedContentAccess: Not logged in, redirect to login page"); - routeValues = await SetPublishedContentAsOtherPageAsync(httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result!.LoginNodeId); - break; - case PublicAccessStatus.AccessDenied: - _logger.LogDebug("EnsurePublishedContentAccess: Current member has not access, redirect to error page"); - routeValues = await SetPublishedContentAsOtherPageAsync(httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result!.NoAccessNodeId); - break; - case PublicAccessStatus.LockedOut: - _logger.LogDebug("Current member is locked out, redirect to error page"); - routeValues = await SetPublishedContentAsOtherPageAsync(httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result!.NoAccessNodeId); - break; - case PublicAccessStatus.NotApproved: - _logger.LogDebug("Current member is unapproved, redirect to error page"); - routeValues = await SetPublishedContentAsOtherPageAsync(httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result!.NoAccessNodeId); - break; - case PublicAccessStatus.AccessAccepted: - _logger.LogDebug("Current member has access"); - break; - } - } - else - { - publicAccessStatus = PublicAccessStatus.AccessAccepted; - _logger.LogDebug("EnsurePublishedContentAccess: Page is not protected"); - } - - - //loop until we have access or reached max loops - } while (routeValues != null && publicAccessStatus != PublicAccessStatus.AccessAccepted && i++ < maxLoop); - - if (i == maxLoop) - { - _logger.LogDebug(nameof(RewriteForPublishedContentAccessAsync) + ": Looks like we are running into an infinite loop, abort"); + return routeValues; } - return routeValues; - } + var path = publishedContent.Path; + Attempt publicAccessAttempt = _publicAccessService.IsProtected(path); - - private async Task SetPublishedContentAsOtherPageAsync(HttpContext httpContext, IPublishedRequest? publishedRequest, int pageId) - { - if (pageId != publishedRequest?.PublishedContent?.Id) + if (publicAccessAttempt.Success) { - var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); - IPublishedContent? publishedContent = umbracoContext.PublishedSnapshot.Content?.GetById(pageId); - if (publishedContent is null || publishedRequest is null) + _logger.LogDebug("EnsurePublishedContentAccess: Page is protected, check for access"); + + // manually authenticate the request + AuthenticateResult authResult = + await httpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme); + if (authResult.Succeeded) { - throw new InvalidOperationException("No content found by id " + pageId); + // set the user to the auth result. we need to do this here because this occurs + // before the authentication middleware. + // NOTE: It would be possible to just pass the authResult to the HasMemberAccessToContentAsync method + // instead of relying directly on the user assigned to the http context, and then the auth middleware + // will run anyways and assign the user. Perhaps that is a little cleaner, but would require more code + // changes right now, and really it's not any different in the end result. + httpContext.SetPrincipalForRequest(authResult.Principal); } - IPublishedRequest reRouted = await _publishedRouter.UpdateRequestAsync(publishedRequest, publishedContent); - - // we need to change the content item that is getting rendered so we have to re-create UmbracoRouteValues. - UmbracoRouteValues updatedRouteValues = await _umbracoRouteValuesFactory.CreateAsync(httpContext, reRouted); - - return updatedRouteValues; + publicAccessStatus = await _publicAccessChecker.HasMemberAccessToContentAsync(publishedContent.Id); + switch (publicAccessStatus) + { + case PublicAccessStatus.NotLoggedIn: + _logger.LogDebug("EnsurePublishedContentAccess: Not logged in, redirect to login page"); + routeValues = await SetPublishedContentAsOtherPageAsync( + httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result!.LoginNodeId); + break; + case PublicAccessStatus.AccessDenied: + _logger.LogDebug( + "EnsurePublishedContentAccess: Current member has not access, redirect to error page"); + routeValues = await SetPublishedContentAsOtherPageAsync( + httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result!.NoAccessNodeId); + break; + case PublicAccessStatus.LockedOut: + _logger.LogDebug("Current member is locked out, redirect to error page"); + routeValues = await SetPublishedContentAsOtherPageAsync( + httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result!.NoAccessNodeId); + break; + case PublicAccessStatus.NotApproved: + _logger.LogDebug("Current member is unapproved, redirect to error page"); + routeValues = await SetPublishedContentAsOtherPageAsync( + httpContext, routeValues.PublishedRequest, publicAccessAttempt.Result!.NoAccessNodeId); + break; + case PublicAccessStatus.AccessAccepted: + _logger.LogDebug("Current member has access"); + break; + } } else { - throw new InvalidOperationException("Public Access rule has a redirect node set to itself, nothing can be routed."); + publicAccessStatus = PublicAccessStatus.AccessAccepted; + _logger.LogDebug("EnsurePublishedContentAccess: Page is not protected"); } + + // loop until we have access or reached max loops + } while (publicAccessStatus != PublicAccessStatus.AccessAccepted && i++ < maxLoop); + + if (i == maxLoop) + { + _logger.LogDebug(nameof(RewriteForPublishedContentAccessAsync) + + ": Looks like we are running into an infinite loop, abort"); } + + return routeValues; + } + + private async Task SetPublishedContentAsOtherPageAsync( + HttpContext httpContext, IPublishedRequest? publishedRequest, int pageId) + { + if (pageId != publishedRequest?.PublishedContent?.Id) + { + IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext(); + IPublishedContent? publishedContent = umbracoContext.PublishedSnapshot.Content?.GetById(pageId); + if (publishedContent is null || publishedRequest is null) + { + throw new InvalidOperationException("No content found by id " + pageId); + } + + IPublishedRequest reRouted = await _publishedRouter.UpdateRequestAsync(publishedRequest, publishedContent); + + // we need to change the content item that is getting rendered so we have to re-create UmbracoRouteValues. + UmbracoRouteValues updatedRouteValues = await _umbracoRouteValuesFactory.CreateAsync(httpContext, reRouted); + + return updatedRouteValues; + } + + throw new InvalidOperationException( + "Public Access rule has a redirect node set to itself, nothing can be routed."); } } diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index f8398eac2d..d4394d2e2a 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Controllers; @@ -11,7 +6,6 @@ using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; @@ -26,258 +20,267 @@ using Umbraco.Extensions; using static Umbraco.Cms.Core.Constants.Web.Routing; using RouteDirection = Umbraco.Cms.Core.Routing.RouteDirection; -namespace Umbraco.Cms.Web.Website.Routing +namespace Umbraco.Cms.Web.Website.Routing; + +/// +/// The route value transformer for Umbraco front-end routes +/// +/// +/// NOTE: In aspnet 5 DynamicRouteValueTransformer has been improved, see +/// https://github.com/dotnet/aspnetcore/issues/21471 +/// It seems as though with the "State" parameter we could more easily assign the IPublishedRequest or +/// IPublishedContent +/// or UmbracoContext more easily that way. In the meantime we will rely on assigning the IPublishedRequest to the +/// route values along with the IPublishedContent to the umbraco context +/// have created a GH discussion here https://github.com/dotnet/aspnetcore/discussions/28562 we'll see if anyone +/// responds +/// +public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer { + private readonly IControllerActionSearcher _controllerActionSearcher; + private readonly IDataProtectionProvider _dataProtectionProvider; + private readonly ILogger _logger; + private readonly IPublicAccessRequestHandler _publicAccessRequestHandler; + private readonly IPublishedRouter _publishedRouter; + private readonly IRoutableDocumentFilter _routableDocumentFilter; + private readonly IUmbracoRouteValuesFactory _routeValuesFactory; + private readonly IRuntimeState _runtime; + private readonly IUmbracoContextAccessor _umbracoContextAccessor; + + [Obsolete("Please use constructor that does not take IOptions, IHostingEnvironment & IEventAggregator instead")] + public UmbracoRouteValueTransformer( + ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedRouter publishedRouter, + IOptions globalSettings, + IHostingEnvironment hostingEnvironment, + IRuntimeState runtime, + IUmbracoRouteValuesFactory routeValuesFactory, + IRoutableDocumentFilter routableDocumentFilter, + IDataProtectionProvider dataProtectionProvider, + IControllerActionSearcher controllerActionSearcher, + IEventAggregator eventAggregator, + IPublicAccessRequestHandler publicAccessRequestHandler) + : this(logger, umbracoContextAccessor, publishedRouter, runtime, routeValuesFactory, routableDocumentFilter, dataProtectionProvider, controllerActionSearcher, publicAccessRequestHandler) + { + } /// - /// The route value transformer for Umbraco front-end routes + /// Initializes a new instance of the class. /// - /// - /// NOTE: In aspnet 5 DynamicRouteValueTransformer has been improved, see https://github.com/dotnet/aspnetcore/issues/21471 - /// It seems as though with the "State" parameter we could more easily assign the IPublishedRequest or IPublishedContent - /// or UmbracoContext more easily that way. In the meantime we will rely on assigning the IPublishedRequest to the - /// route values along with the IPublishedContent to the umbraco context - /// have created a GH discussion here https://github.com/dotnet/aspnetcore/discussions/28562 we'll see if anyone responds - /// - public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer + public UmbracoRouteValueTransformer( + ILogger logger, + IUmbracoContextAccessor umbracoContextAccessor, + IPublishedRouter publishedRouter, + IRuntimeState runtime, + IUmbracoRouteValuesFactory routeValuesFactory, + IRoutableDocumentFilter routableDocumentFilter, + IDataProtectionProvider dataProtectionProvider, + IControllerActionSearcher controllerActionSearcher, + IPublicAccessRequestHandler publicAccessRequestHandler) { - private readonly ILogger _logger; - private readonly IUmbracoContextAccessor _umbracoContextAccessor; - private readonly IPublishedRouter _publishedRouter; - private readonly GlobalSettings _globalSettings; - private readonly IHostingEnvironment _hostingEnvironment; - private readonly IRuntimeState _runtime; - private readonly IUmbracoRouteValuesFactory _routeValuesFactory; - private readonly IRoutableDocumentFilter _routableDocumentFilter; - private readonly IDataProtectionProvider _dataProtectionProvider; - private readonly IControllerActionSearcher _controllerActionSearcher; - private readonly IEventAggregator _eventAggregator; - private readonly IPublicAccessRequestHandler _publicAccessRequestHandler; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _umbracoContextAccessor = + umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); + _publishedRouter = publishedRouter ?? throw new ArgumentNullException(nameof(publishedRouter)); + _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); + _routeValuesFactory = routeValuesFactory ?? throw new ArgumentNullException(nameof(routeValuesFactory)); + _routableDocumentFilter = + routableDocumentFilter ?? throw new ArgumentNullException(nameof(routableDocumentFilter)); + _dataProtectionProvider = dataProtectionProvider; + _controllerActionSearcher = controllerActionSearcher; + _publicAccessRequestHandler = publicAccessRequestHandler; + } - /// - /// Initializes a new instance of the class. - /// - public UmbracoRouteValueTransformer( - ILogger logger, - IUmbracoContextAccessor umbracoContextAccessor, - IPublishedRouter publishedRouter, - IOptions globalSettings, - IHostingEnvironment hostingEnvironment, - IRuntimeState runtime, - IUmbracoRouteValuesFactory routeValuesFactory, - IRoutableDocumentFilter routableDocumentFilter, - IDataProtectionProvider dataProtectionProvider, - IControllerActionSearcher controllerActionSearcher, - IEventAggregator eventAggregator, - IPublicAccessRequestHandler publicAccessRequestHandler) + /// + public override async ValueTask TransformAsync( + HttpContext httpContext, RouteValueDictionary values) + { + // If we aren't running, then we have nothing to route + if (_runtime.Level != RuntimeLevel.Run) { - if (globalSettings is null) - { - throw new ArgumentNullException(nameof(globalSettings)); - } - - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor)); - _publishedRouter = publishedRouter ?? throw new ArgumentNullException(nameof(publishedRouter)); - _globalSettings = globalSettings.Value; - _hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); - _runtime = runtime ?? throw new ArgumentNullException(nameof(runtime)); - _routeValuesFactory = routeValuesFactory ?? throw new ArgumentNullException(nameof(routeValuesFactory)); - _routableDocumentFilter = routableDocumentFilter ?? throw new ArgumentNullException(nameof(routableDocumentFilter)); - _dataProtectionProvider = dataProtectionProvider; - _controllerActionSearcher = controllerActionSearcher; - _eventAggregator = eventAggregator; - _publicAccessRequestHandler = publicAccessRequestHandler; + return null!; } - /// - public override async ValueTask TransformAsync(HttpContext httpContext, RouteValueDictionary values) + // will be null for any client side requests like JS, etc... + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) { - // If we aren't running, then we have nothing to route - if (_runtime.Level != RuntimeLevel.Run) + return null!; + } + + if (!_routableDocumentFilter.IsDocumentRequest(httpContext.Request.Path)) + { + return null!; + } + + // Don't execute if there are already UmbracoRouteValues assigned. + // This can occur if someone else is dynamically routing and in which case we don't want to overwrite + // the routing work being done there. + UmbracoRouteValues? umbracoRouteValues = httpContext.Features.Get(); + if (umbracoRouteValues != null) + { + return null!; + } + + // Check if there is no existing content and return the no content controller + if (!umbracoContext.Content?.HasContent() ?? false) + { + return new RouteValueDictionary { - return null!; - } - // will be null for any client side requests like JS, etc... - if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext)) - { - return null!; - } - - if (!_routableDocumentFilter.IsDocumentRequest(httpContext.Request.Path)) - { - return null!; - } - - // Don't execute if there are already UmbracoRouteValues assigned. - // This can occur if someone else is dynamically routing and in which case we don't want to overwrite - // the routing work being done there. - UmbracoRouteValues? umbracoRouteValues = httpContext.Features.Get(); - if (umbracoRouteValues != null) - { - return null!; - } - - // Check if there is no existing content and return the no content controller - if (!umbracoContext.Content?.HasContent() ?? false) - { - return new RouteValueDictionary - { - [ControllerToken] = ControllerExtensions.GetControllerName(), - [ActionToken] = nameof(RenderNoContentController.Index) - }; - } - - IPublishedRequest publishedRequest = await RouteRequestAsync(umbracoContext); - - umbracoRouteValues = await _routeValuesFactory.CreateAsync(httpContext, publishedRequest); - - // now we need to do some public access checks - umbracoRouteValues = await _publicAccessRequestHandler.RewriteForPublishedContentAccessAsync(httpContext, umbracoRouteValues); - - // Store the route values as a httpcontext feature - httpContext.Features.Set(umbracoRouteValues); - - // Need to check if there is form data being posted back to an Umbraco URL - PostedDataProxyInfo? postedInfo = GetFormInfo(httpContext, values); - if (postedInfo != null) - { - return HandlePostedValues(postedInfo, httpContext); - } - - UmbracoRouteResult? routeResult = umbracoRouteValues?.PublishedRequest?.GetRouteResult(); - - if (!routeResult.HasValue || routeResult == UmbracoRouteResult.NotFound) - { - // No content was found, not by any registered 404 handlers and - // not by the IContentLastChanceFinder. In this case we want to return - // 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!; - } - - // See https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.dynamicroutevaluetransformer.transformasync?view=aspnetcore-5.0#Microsoft_AspNetCore_Mvc_Routing_DynamicRouteValueTransformer_TransformAsync_Microsoft_AspNetCore_Http_HttpContext_Microsoft_AspNetCore_Routing_RouteValueDictionary_ - // We should apparenlty not be modified these values. - // So we create new ones. - var newValues = new RouteValueDictionary - { - [ControllerToken] = umbracoRouteValues?.ControllerName + [ControllerToken] = ControllerExtensions.GetControllerName(), + [ActionToken] = nameof(RenderNoContentController.Index), }; - if (string.IsNullOrWhiteSpace(umbracoRouteValues?.ActionName) == false) - { - newValues[ActionToken] = umbracoRouteValues.ActionName; - } - - return newValues; } - private async Task RouteRequestAsync(IUmbracoContext umbracoContext) + IPublishedRequest publishedRequest = await RouteRequestAsync(umbracoContext); + + umbracoRouteValues = await _routeValuesFactory.CreateAsync(httpContext, publishedRequest); + + // now we need to do some public access checks + umbracoRouteValues = + await _publicAccessRequestHandler.RewriteForPublishedContentAccessAsync(httpContext, umbracoRouteValues); + + // Store the route values as a httpcontext feature + httpContext.Features.Set(umbracoRouteValues); + + // Need to check if there is form data being posted back to an Umbraco URL + PostedDataProxyInfo? postedInfo = GetFormInfo(httpContext, values); + if (postedInfo != null) { - // ok, process - - // instantiate, prepare and process the published content request - // important to use CleanedUmbracoUrl - lowercase path-only version of the current url - IPublishedRequestBuilder requestBuilder = await _publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); - - // TODO: This is ugly with the re-assignment to umbraco context but at least its now - // an immutable object. The only way to make this better would be to have a RouteRequest - // as part of UmbracoContext but then it will require a PublishedRouter dependency so not sure that's worth it. - // Maybe could be a one-time Set method instead? - IPublishedRequest routedRequest = await _publishedRouter.RouteRequestAsync(requestBuilder, new RouteRequestOptions(RouteDirection.Inbound)); - umbracoContext.PublishedRequest = routedRequest; - - return routedRequest; + return HandlePostedValues(postedInfo, httpContext); } - /// - /// Checks the request and query strings to see if it matches the definition of having a Surface controller - /// posted/get value, if so, then we return a PostedDataProxyInfo object with the correct information. - /// - private PostedDataProxyInfo? GetFormInfo(HttpContext httpContext, RouteValueDictionary values) + UmbracoRouteResult? routeResult = umbracoRouteValues?.PublishedRequest.GetRouteResult(); + + if (!routeResult.HasValue || routeResult == UmbracoRouteResult.NotFound) { - if (httpContext is null) - { - throw new ArgumentNullException(nameof(httpContext)); - } + // No content was found, not by any registered 404 handlers and + // not by the IContentLastChanceFinder. In this case we want to return + // 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!; + } - // if it is a POST/GET then a `ufprt` value must be in the request - var ufprt = httpContext.Request.GetUfprt(); - if (string.IsNullOrWhiteSpace(ufprt)) - { - return null; - } + // See https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.dynamicroutevaluetransformer.transformasync?view=aspnetcore-5.0#Microsoft_AspNetCore_Mvc_Routing_DynamicRouteValueTransformer_TransformAsync_Microsoft_AspNetCore_Http_HttpContext_Microsoft_AspNetCore_Routing_RouteValueDictionary_ + // We should apparenlty not be modified these values. + // So we create new ones. + var newValues = new RouteValueDictionary { [ControllerToken] = umbracoRouteValues?.ControllerName }; + if (string.IsNullOrWhiteSpace(umbracoRouteValues?.ActionName) == false) + { + newValues[ActionToken] = umbracoRouteValues.ActionName; + } - if (!EncryptionHelper.DecryptAndValidateEncryptedRouteString( + return newValues; + } + + private async Task RouteRequestAsync(IUmbracoContext umbracoContext) + { + // ok, process + + // instantiate, prepare and process the published content request + // important to use CleanedUmbracoUrl - lowercase path-only version of the current url + IPublishedRequestBuilder requestBuilder = + await _publishedRouter.CreateRequestAsync(umbracoContext.CleanedUmbracoUrl); + + // TODO: This is ugly with the re-assignment to umbraco context but at least its now + // an immutable object. The only way to make this better would be to have a RouteRequest + // as part of UmbracoContext but then it will require a PublishedRouter dependency so not sure that's worth it. + // Maybe could be a one-time Set method instead? + IPublishedRequest routedRequest = + await _publishedRouter.RouteRequestAsync(requestBuilder, new RouteRequestOptions(RouteDirection.Inbound)); + umbracoContext.PublishedRequest = routedRequest; + + return routedRequest; + } + + /// + /// Checks the request and query strings to see if it matches the definition of having a Surface controller + /// posted/get value, if so, then we return a PostedDataProxyInfo object with the correct information. + /// + private PostedDataProxyInfo? GetFormInfo(HttpContext httpContext, RouteValueDictionary values) + { + if (httpContext is null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + // if it is a POST/GET then a `ufprt` value must be in the request + var ufprt = httpContext.Request.GetUfprt(); + if (string.IsNullOrWhiteSpace(ufprt)) + { + return null; + } + + if (!EncryptionHelper.DecryptAndValidateEncryptedRouteString( _dataProtectionProvider, ufprt, out IDictionary? decodedUfprt)) - { - return null; - } - - // Get all route values that are not the default ones and add them separately so they eventually get to action parameters - foreach (KeyValuePair item in decodedUfprt.Where(x => ReservedAdditionalKeys.AllKeys.Contains(x.Key) == false)) - { - values[item.Key] = item.Value; - } - - // return the proxy info without the surface id... could be a local controller. - return new PostedDataProxyInfo - { - ControllerName = WebUtility.UrlDecode(decodedUfprt.First(x => x.Key == ReservedAdditionalKeys.Controller).Value), - ActionName = WebUtility.UrlDecode(decodedUfprt.First(x => x.Key == ReservedAdditionalKeys.Action).Value), - Area = WebUtility.UrlDecode(decodedUfprt.First(x => x.Key == ReservedAdditionalKeys.Area).Value), - }; - } - - private RouteValueDictionary HandlePostedValues(PostedDataProxyInfo postedInfo, HttpContext httpContext) { - // set the standard route values/tokens - var values = new RouteValueDictionary - { - [ControllerToken] = postedInfo.ControllerName, - [ActionToken] = postedInfo.ActionName - }; - - ControllerActionDescriptor? surfaceControllerDescriptor = _controllerActionSearcher.Find(httpContext, postedInfo.ControllerName, postedInfo.ActionName, postedInfo.Area); - - if (surfaceControllerDescriptor == null) - { - throw new InvalidOperationException("Could not find a Surface controller route in the RouteTable for controller name " + postedInfo.ControllerName); - } - - // set the area if one is there. - if (!postedInfo.Area.IsNullOrWhiteSpace()) - { - values["area"] = postedInfo.Area; - } - - return values; + return null; } - private class PostedDataProxyInfo + // Get all route values that are not the default ones and add them separately so they eventually get to action parameters + foreach (KeyValuePair item in decodedUfprt.Where(x => + ReservedAdditionalKeys.AllKeys.Contains(x.Key) == false)) { - public string? ControllerName { get; set; } - - public string? ActionName { get; set; } - - public string? Area { get; set; } + values[item.Key] = item.Value; } - // Define reserved dictionary keys for controller, action and area specified in route additional values data - private static class ReservedAdditionalKeys + // return the proxy info without the surface id... could be a local controller. + return new PostedDataProxyInfo { - internal static readonly string[] AllKeys = new[] - { - Controller, - Action, - Area - }; + ControllerName = + WebUtility.UrlDecode(decodedUfprt.First(x => x.Key == ReservedAdditionalKeys.Controller).Value), + ActionName = + WebUtility.UrlDecode(decodedUfprt.First(x => x.Key == ReservedAdditionalKeys.Action).Value), + Area = WebUtility.UrlDecode(decodedUfprt.First(x => x.Key == ReservedAdditionalKeys.Area).Value), + }; + } - internal const string Controller = "c"; - internal const string Action = "a"; - internal const string Area = "ar"; + private RouteValueDictionary HandlePostedValues(PostedDataProxyInfo postedInfo, HttpContext httpContext) + { + // set the standard route values/tokens + var values = new RouteValueDictionary + { + [ControllerToken] = postedInfo.ControllerName, [ActionToken] = postedInfo.ActionName, + }; + + ControllerActionDescriptor? surfaceControllerDescriptor = + _controllerActionSearcher.Find(httpContext, postedInfo.ControllerName, postedInfo.ActionName, postedInfo.Area); + + if (surfaceControllerDescriptor == null) + { + throw new InvalidOperationException( + "Could not find a Surface controller route in the RouteTable for controller name " + + postedInfo.ControllerName); } + + // set the area if one is there. + if (!postedInfo.Area.IsNullOrWhiteSpace()) + { + values["area"] = postedInfo.Area; + } + + return values; + } + + private class PostedDataProxyInfo + { + public string? ControllerName { get; set; } + + public string? ActionName { get; set; } + + public string? Area { get; set; } + } + + // Define reserved dictionary keys for controller, action and area specified in route additional values data + private static class ReservedAdditionalKeys + { + internal const string Controller = "c"; + internal const string Action = "a"; + internal const string Area = "ar"; + + internal static readonly string[] AllKeys = { Controller, Action, Area }; } } diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs index 48348606f1..8eb181003f 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValuesFactory.cs @@ -1,5 +1,3 @@ -using System; -using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.Extensions.Options; @@ -12,162 +10,168 @@ using Umbraco.Cms.Web.Common.Routing; using Umbraco.Cms.Web.Website.Controllers; using Umbraco.Extensions; -namespace Umbraco.Cms.Web.Website.Routing +namespace Umbraco.Cms.Web.Website.Routing; + +/// +/// Used to create +/// +public class UmbracoRouteValuesFactory : IUmbracoRouteValuesFactory { + private readonly IControllerActionSearcher _controllerActionSearcher; + private readonly Lazy _defaultControllerDescriptor; + private readonly Lazy _defaultControllerName; + private readonly IPublishedRouter _publishedRouter; + private readonly IShortStringHelper _shortStringHelper; + private readonly UmbracoFeatures _umbracoFeatures; + /// - /// Used to create + /// Initializes a new instance of the class. /// - public class UmbracoRouteValuesFactory : IUmbracoRouteValuesFactory + public UmbracoRouteValuesFactory( + IOptions renderingDefaults, + IShortStringHelper shortStringHelper, + UmbracoFeatures umbracoFeatures, + IControllerActionSearcher controllerActionSearcher, + IPublishedRouter publishedRouter) { - private readonly IShortStringHelper _shortStringHelper; - private readonly UmbracoFeatures _umbracoFeatures; - private readonly IControllerActionSearcher _controllerActionSearcher; - private readonly IPublishedRouter _publishedRouter; - private readonly Lazy _defaultControllerName; - private readonly Lazy _defaultControllerDescriptor; - - /// - /// Initializes a new instance of the class. - /// - public UmbracoRouteValuesFactory( - IOptions renderingDefaults, - IShortStringHelper shortStringHelper, - UmbracoFeatures umbracoFeatures, - IControllerActionSearcher controllerActionSearcher, - IPublishedRouter publishedRouter) + _shortStringHelper = shortStringHelper; + _umbracoFeatures = umbracoFeatures; + _controllerActionSearcher = controllerActionSearcher; + _publishedRouter = publishedRouter; + _defaultControllerName = new Lazy(() => + ControllerExtensions.GetControllerName(renderingDefaults.Value.DefaultControllerType)); + _defaultControllerDescriptor = new Lazy(() => { - _shortStringHelper = shortStringHelper; - _umbracoFeatures = umbracoFeatures; - _controllerActionSearcher = controllerActionSearcher; - _publishedRouter = publishedRouter; - _defaultControllerName = new Lazy(() => ControllerExtensions.GetControllerName(renderingDefaults.Value.DefaultControllerType)); - _defaultControllerDescriptor = new Lazy(() => + ControllerActionDescriptor? descriptor = _controllerActionSearcher.Find( + new DefaultHttpContext(), // this actually makes no difference for this method + DefaultControllerName, + UmbracoRouteValues.DefaultActionName); + + if (descriptor == null) { - ControllerActionDescriptor? descriptor = _controllerActionSearcher.Find( - new DefaultHttpContext(), // this actually makes no difference for this method - DefaultControllerName, - UmbracoRouteValues.DefaultActionName); + throw new InvalidOperationException( + $"No controller/action found by name {DefaultControllerName}.{UmbracoRouteValues.DefaultActionName}"); + } - if (descriptor == null) - { - throw new InvalidOperationException($"No controller/action found by name {DefaultControllerName}.{UmbracoRouteValues.DefaultActionName}"); - } + return descriptor; + }); + } - return descriptor; - }); + /// + /// Gets the default controller name + /// + protected string DefaultControllerName => _defaultControllerName.Value; + + /// + public async Task CreateAsync(HttpContext httpContext, IPublishedRequest request) + { + if (httpContext is null) + { + throw new ArgumentNullException(nameof(httpContext)); } - /// - /// Gets the default controller name - /// - protected string DefaultControllerName => _defaultControllerName.Value; - - /// - public async Task CreateAsync(HttpContext httpContext, IPublishedRequest request) + if (request is null) { - if (httpContext is null) + throw new ArgumentNullException(nameof(request)); + } + + string? customActionName = null; + + // check that a template is defined), if it doesn't and there is a hijacked route it will just route + // to the index Action + if (request.HasTemplate()) + { + // the template Alias should always be already saved with a safe name. + // if there are hyphens in the name and there is a hijacked route, then the Action will need to be attributed + // with the action name attribute. + customActionName = request.GetTemplateAlias()?.Split('.')[0].ToSafeAlias(_shortStringHelper); + } + + // The default values for the default controller/action + var def = new UmbracoRouteValues( + request, + _defaultControllerDescriptor.Value, + customActionName); + + def = CheckHijackedRoute(httpContext, def, out var hasHijackedRoute); + + def = await CheckNoTemplateAsync(httpContext, def, hasHijackedRoute); + + return def; + } + + /// + /// Check if the route is hijacked and return new route values + /// + private UmbracoRouteValues CheckHijackedRoute( + HttpContext httpContext, UmbracoRouteValues def, out bool hasHijackedRoute) + { + IPublishedRequest request = def.PublishedRequest; + + var customControllerName = request.PublishedContent?.ContentType?.Alias; + if (customControllerName != null) + { + ControllerActionDescriptor? descriptor = + _controllerActionSearcher.Find(httpContext, customControllerName, def.TemplateName); + if (descriptor != null) { - throw new ArgumentNullException(nameof(httpContext)); + hasHijackedRoute = true; + + return new UmbracoRouteValues( + request, + descriptor, + def.TemplateName); + } + } + + hasHijackedRoute = false; + return def; + } + + /// + /// Special check for when no template or hijacked route is done which needs to re-run through the routing pipeline + /// again for last chance finders + /// + private async Task CheckNoTemplateAsync( + HttpContext httpContext, UmbracoRouteValues def, bool hasHijackedRoute) + { + IPublishedRequest request = def.PublishedRequest; + + // Here we need to check if there is no hijacked route and no template assigned but there is a content item. + // If this is the case we want to return a blank page. + // We also check if templates have been disabled since if they are then we're allowed to render even though there's no template, + // for example for json rendering in headless. + if (request.HasPublishedContent() + && !request.HasTemplate() + && !_umbracoFeatures.Disabled.DisableTemplates + && !hasHijackedRoute) + { + IPublishedContent? content = request.PublishedContent; + + // This is basically a 404 even if there is content found. + // We then need to re-run this through the pipeline for the last + // chance finders to work. + // Set to null since we are telling it there is no content. + request = await _publishedRouter.UpdateRequestAsync(request, null); + + if (request == null) + { + throw new InvalidOperationException( + $"The call to {nameof(IPublishedRouter.UpdateRequestAsync)} cannot return null"); } - if (request is null) - { - throw new ArgumentNullException(nameof(request)); - } - - string? customActionName = null; - - // check that a template is defined), if it doesn't and there is a hijacked route it will just route - // to the index Action - if (request.HasTemplate()) - { - // the template Alias should always be already saved with a safe name. - // if there are hyphens in the name and there is a hijacked route, then the Action will need to be attributed - // with the action name attribute. - customActionName = request.GetTemplateAlias()?.Split('.')[0].ToSafeAlias(_shortStringHelper); - } - - // The default values for the default controller/action - var def = new UmbracoRouteValues( + def = new UmbracoRouteValues( request, - _defaultControllerDescriptor.Value, - templateName: customActionName); + def.ControllerActionDescriptor, + def.TemplateName); - def = CheckHijackedRoute(httpContext, def, out bool hasHijackedRoute); - - def = await CheckNoTemplateAsync(httpContext, def, hasHijackedRoute); - - return def; - } - - /// - /// Check if the route is hijacked and return new route values - /// - private UmbracoRouteValues CheckHijackedRoute(HttpContext httpContext, UmbracoRouteValues def, out bool hasHijackedRoute) - { - IPublishedRequest request = def.PublishedRequest; - - var customControllerName = request.PublishedContent?.ContentType?.Alias; - if (customControllerName != null) + // if the content has changed, we must then again check for hijacked routes + if (content != request.PublishedContent) { - ControllerActionDescriptor? descriptor = _controllerActionSearcher.Find(httpContext, customControllerName, def.TemplateName); - if (descriptor != null) - { - hasHijackedRoute = true; - - return new UmbracoRouteValues( - request, - descriptor, - def.TemplateName); - } + def = CheckHijackedRoute(httpContext, def, out _); } - - hasHijackedRoute = false; - return def; } - /// - /// Special check for when no template or hijacked route is done which needs to re-run through the routing pipeline again for last chance finders - /// - private async Task CheckNoTemplateAsync(HttpContext httpContext, UmbracoRouteValues def, bool hasHijackedRoute) - { - IPublishedRequest request = def.PublishedRequest; - - // Here we need to check if there is no hijacked route and no template assigned but there is a content item. - // If this is the case we want to return a blank page. - // We also check if templates have been disabled since if they are then we're allowed to render even though there's no template, - // for example for json rendering in headless. - if (request.HasPublishedContent() - && !request.HasTemplate() - && !_umbracoFeatures.Disabled.DisableTemplates - && !hasHijackedRoute) - { - IPublishedContent? content = request.PublishedContent; - - // This is basically a 404 even if there is content found. - // We then need to re-run this through the pipeline for the last - // chance finders to work. - // Set to null since we are telling it there is no content. - request = await _publishedRouter.UpdateRequestAsync(request, null); - - if (request == null) - { - throw new InvalidOperationException($"The call to {nameof(IPublishedRouter.UpdateRequestAsync)} cannot return null"); - } - - def = new UmbracoRouteValues( - request, - def.ControllerActionDescriptor, - def.TemplateName); - - // if the content has changed, we must then again check for hijacked routes - if (content != request.PublishedContent) - { - def = CheckHijackedRoute(httpContext, def, out _); - } - } - - return def; - } + return def; } } diff --git a/src/Umbraco.Web.Website/Security/MemberAuthenticationBuilder.cs b/src/Umbraco.Web.Website/Security/MemberAuthenticationBuilder.cs index a81cbfe73d..0029e0b80a 100644 --- a/src/Umbraco.Web.Website/Security/MemberAuthenticationBuilder.cs +++ b/src/Umbraco.Web.Website/Security/MemberAuthenticationBuilder.cs @@ -1,75 +1,75 @@ -using System; -using System.Diagnostics; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; using Umbraco.Cms.Web.Common.Security; using Umbraco.Extensions; -using Constants = Umbraco.Cms.Core.Constants; -namespace Umbraco.Cms.Web.Website.Security +namespace Umbraco.Cms.Web.Website.Security; + +/// +/// Custom used to associate external logins with umbraco external login options +/// +public class MemberAuthenticationBuilder : AuthenticationBuilder { + private readonly Action _loginProviderOptions; + + public MemberAuthenticationBuilder( + IServiceCollection services, + Action? loginProviderOptions = null) + : base(services) + => _loginProviderOptions = loginProviderOptions ?? (x => { }); + + public string SchemeForMembers(string scheme) + => scheme.EnsureStartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix); + /// - /// Custom used to associate external logins with umbraco external login options + /// Overridden to track the final authenticationScheme being registered for the external login /// - public class MemberAuthenticationBuilder : AuthenticationBuilder + /// + /// + /// + /// + /// + /// + public override AuthenticationBuilder AddRemoteScheme( + string authenticationScheme, string? displayName, Action? configureOptions) { - private readonly Action _loginProviderOptions; - - public MemberAuthenticationBuilder( - IServiceCollection services, - Action? loginProviderOptions = null) - : base(services) - => _loginProviderOptions = loginProviderOptions ?? (x => { }); - - public string? SchemeForMembers(string scheme) - => scheme?.EnsureStartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix); - - /// - /// Overridden to track the final authenticationScheme being registered for the external login - /// - /// - /// - /// - /// - /// - /// - public override AuthenticationBuilder AddRemoteScheme(string authenticationScheme, string? displayName, Action? configureOptions) + // Validate that the prefix is set + if (!authenticationScheme.StartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix)) { - // Validate that the prefix is set - if (!authenticationScheme.StartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix)) - { - throw new InvalidOperationException($"The {nameof(authenticationScheme)} is not prefixed with {Constants.Security.MemberExternalAuthenticationTypePrefix}. The scheme must be created with a call to the method {nameof(SchemeForMembers)}"); - } - - // add our login provider to the container along with a custom options configuration - Services.Configure(authenticationScheme, _loginProviderOptions); - base.Services.AddSingleton(services => - { - return new MemberExternalLoginProvider( - authenticationScheme, - services.GetRequiredService>()); - }); - Services.TryAddEnumerable(ServiceDescriptor.Singleton, EnsureMemberScheme>()); - - return base.AddRemoteScheme(authenticationScheme, displayName, configureOptions); + throw new InvalidOperationException( + $"The {nameof(authenticationScheme)} is not prefixed with {Constants.Security.MemberExternalAuthenticationTypePrefix}. The scheme must be created with a call to the method {nameof(SchemeForMembers)}"); } - // Ensures that the sign in scheme is always the Umbraco member external type - private class EnsureMemberScheme : IPostConfigureOptions where TOptions : RemoteAuthenticationOptions + // add our login provider to the container along with a custom options configuration + Services.Configure(authenticationScheme, _loginProviderOptions); + Services.AddSingleton(services => { - public void PostConfigure(string name, TOptions options) - { - if (!name.StartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix)) - { - return; - } + return new MemberExternalLoginProvider( + authenticationScheme, + services.GetRequiredService>()); + }); + Services.TryAddEnumerable(ServiceDescriptor + .Singleton, EnsureMemberScheme>()); - options.SignInScheme = IdentityConstants.ExternalScheme; - } - } + return base.AddRemoteScheme(authenticationScheme, displayName, configureOptions); } + // Ensures that the sign in scheme is always the Umbraco member external type + private class EnsureMemberScheme : IPostConfigureOptions + where TOptions : RemoteAuthenticationOptions + { + public void PostConfigure(string name, TOptions options) + { + if (!name.StartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix)) + { + return; + } + + options.SignInScheme = IdentityConstants.ExternalScheme; + } + } } diff --git a/src/Umbraco.Web.Website/Security/MemberExternalLoginsBuilder.cs b/src/Umbraco.Web.Website/Security/MemberExternalLoginsBuilder.cs index 22583e2cd8..4876263ffd 100644 --- a/src/Umbraco.Web.Website/Security/MemberExternalLoginsBuilder.cs +++ b/src/Umbraco.Web.Website/Security/MemberExternalLoginsBuilder.cs @@ -1,34 +1,28 @@ -using System; using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Web.Common.Security; -namespace Umbraco.Cms.Web.Website.Security +namespace Umbraco.Cms.Web.Website.Security; + +/// +/// Used to add back office login providers +/// +public class MemberExternalLoginsBuilder { + private readonly IServiceCollection _services; + + public MemberExternalLoginsBuilder(IServiceCollection services) => _services = services; + /// - /// Used to add back office login providers + /// Add a back office login provider with options /// - public class MemberExternalLoginsBuilder + /// + /// + /// + public MemberExternalLoginsBuilder AddMemberLogin( + Action build, + Action? loginProviderOptions = null) { - public MemberExternalLoginsBuilder(IServiceCollection services) - { - _services = services; - } - - private readonly IServiceCollection _services; - - /// - /// Add a back office login provider with options - /// - /// - /// - /// - public MemberExternalLoginsBuilder AddMemberLogin( - Action build, - Action? loginProviderOptions = null) - { - build(new MemberAuthenticationBuilder(_services, loginProviderOptions)); - return this; - } + build(new MemberAuthenticationBuilder(_services, loginProviderOptions)); + return this; } - } diff --git a/src/Umbraco.Web.Website/ViewEngines/PluginRazorViewEngineOptionsSetup.cs b/src/Umbraco.Web.Website/ViewEngines/PluginRazorViewEngineOptionsSetup.cs index fbf2a34023..b298b5d8ec 100644 --- a/src/Umbraco.Web.Website/ViewEngines/PluginRazorViewEngineOptionsSetup.cs +++ b/src/Umbraco.Web.Website/ViewEngines/PluginRazorViewEngineOptionsSetup.cs @@ -1,53 +1,52 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; -namespace Umbraco.Cms.Web.Website.ViewEngines -{ - /// - /// Configure view engine locations for front-end rendering based on App_Plugins views - /// - public class PluginRazorViewEngineOptionsSetup : IConfigureOptions - { - /// - public void Configure(RazorViewEngineOptions options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } +namespace Umbraco.Cms.Web.Website.ViewEngines; - options.ViewLocationExpanders.Add(new ViewLocationExpander()); +/// +/// Configure view engine locations for front-end rendering based on App_Plugins views +/// +public class PluginRazorViewEngineOptionsSetup : IConfigureOptions +{ + /// + public void Configure(RazorViewEngineOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); } - /// - /// Expands the default view locations - /// - private class ViewLocationExpander : IViewLocationExpander + options.ViewLocationExpanders.Add(new ViewLocationExpander()); + } + + /// + /// Expands the default view locations + /// + private class ViewLocationExpander : IViewLocationExpander + { + public IEnumerable ExpandViewLocations( + ViewLocationExpanderContext context, IEnumerable viewLocations) { - public IEnumerable ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable viewLocations) + string[] umbViewLocations = { - string[] umbViewLocations = new string[] - { - // area view locations for the plugin folder - string.Concat(Constants.SystemDirectories.AppPlugins, "/{2}/Views/{1}/{0}.cshtml"), - string.Concat(Constants.SystemDirectories.AppPlugins, "/{2}/Views/Shared/{0}.cshtml"), + // area view locations for the plugin folder + string.Concat(Constants.SystemDirectories.AppPlugins, "/{2}/Views/{1}/{0}.cshtml"), + string.Concat(Constants.SystemDirectories.AppPlugins, "/{2}/Views/Shared/{0}.cshtml"), - // will be used when we have partial view and child action macros - string.Concat(Constants.SystemDirectories.AppPlugins, "/{2}/Views/Partials/{0}.cshtml"), - string.Concat(Constants.SystemDirectories.AppPlugins, "/{2}/Views/MacroPartials/{0}.cshtml") - }; + // will be used when we have partial view and child action macros + string.Concat(Constants.SystemDirectories.AppPlugins, "/{2}/Views/Partials/{0}.cshtml"), + string.Concat(Constants.SystemDirectories.AppPlugins, "/{2}/Views/MacroPartials/{0}.cshtml"), + }; - viewLocations = umbViewLocations.Concat(viewLocations); + viewLocations = umbViewLocations.Concat(viewLocations); - return viewLocations; - } + return viewLocations; + } - // not a dynamic expander - public void PopulateValues(ViewLocationExpanderContext context) { } + // not a dynamic expander + public void PopulateValues(ViewLocationExpanderContext context) + { } } } diff --git a/src/Umbraco.Web.Website/ViewEngines/ProfilingViewEngine.cs b/src/Umbraco.Web.Website/ViewEngines/ProfilingViewEngine.cs index 7a2cf14c18..f4f2f004e7 100644 --- a/src/Umbraco.Web.Website/ViewEngines/ProfilingViewEngine.cs +++ b/src/Umbraco.Web.Website/ViewEngines/ProfilingViewEngine.cs @@ -1,44 +1,43 @@ -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewEngines; using Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Web.Website.ViewEngines +namespace Umbraco.Cms.Web.Website.ViewEngines; + +public class ProfilingViewEngine : IViewEngine { - public class ProfilingViewEngine: IViewEngine + private readonly string _name; + private readonly IProfiler _profiler; + internal readonly IViewEngine Inner; + + public ProfilingViewEngine(IViewEngine inner, IProfiler profiler) { - internal readonly IViewEngine Inner; - private readonly IProfiler _profiler; - private readonly string _name; + Inner = inner; + _profiler = profiler; + _name = inner.GetType().Name; + } - public ProfilingViewEngine(IViewEngine inner, IProfiler profiler) + public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage) + { + using (_profiler.Step(string.Format("{0}.FindView, {1}, {2}", _name, viewName, isMainPage))) { - Inner = inner; - _profiler = profiler; - _name = inner.GetType().Name; - } - - public ViewEngineResult FindView(ActionContext context, string viewName, bool isMainPage) - { - using (_profiler.Step(string.Format("{0}.FindView, {1}, {2}", _name, viewName, isMainPage))) - { - return WrapResult(Inner.FindView(context, viewName, isMainPage)); - } - } - - private static ViewEngineResult WrapResult(ViewEngineResult innerResult) - { - var profiledResult = innerResult.View is null - ? ViewEngineResult.NotFound(innerResult.ViewName, innerResult.SearchedLocations) - : ViewEngineResult.Found(innerResult.ViewName, innerResult.View); - return profiledResult; - } - - public ViewEngineResult GetView(string? executingFilePath, string viewPath, bool isMainPage) - { - using (_profiler.Step(string.Format("{0}.GetView, {1}, {2}, {3}", _name, executingFilePath, viewPath, isMainPage))) - { - return Inner.GetView(executingFilePath, viewPath, isMainPage); - } + return WrapResult(Inner.FindView(context, viewName, isMainPage)); } } + + public ViewEngineResult GetView(string? executingFilePath, string viewPath, bool isMainPage) + { + using (_profiler.Step(string.Format("{0}.GetView, {1}, {2}, {3}", _name, executingFilePath, viewPath, isMainPage))) + { + return Inner.GetView(executingFilePath, viewPath, isMainPage); + } + } + + private static ViewEngineResult WrapResult(ViewEngineResult innerResult) + { + ViewEngineResult profiledResult = innerResult.View is null + ? ViewEngineResult.NotFound(innerResult.ViewName, innerResult.SearchedLocations) + : ViewEngineResult.Found(innerResult.ViewName, innerResult.View); + return profiledResult; + } } diff --git a/src/Umbraco.Web.Website/ViewEngines/ProfilingViewEngineWrapperMvcViewOptionsSetup.cs b/src/Umbraco.Web.Website/ViewEngines/ProfilingViewEngineWrapperMvcViewOptionsSetup.cs index 673b88208c..56297f1dd7 100644 --- a/src/Umbraco.Web.Website/ViewEngines/ProfilingViewEngineWrapperMvcViewOptionsSetup.cs +++ b/src/Umbraco.Web.Website/ViewEngines/ProfilingViewEngineWrapperMvcViewOptionsSetup.cs @@ -1,51 +1,49 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewEngines; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Logging; -namespace Umbraco.Cms.Web.Website.ViewEngines +namespace Umbraco.Cms.Web.Website.ViewEngines; + +/// +/// Wraps all view engines with a +/// +public class ProfilingViewEngineWrapperMvcViewOptionsSetup : IConfigureOptions { + private readonly IProfiler _profiler; + /// - /// Wraps all view engines with a + /// Initializes a new instance of the class. /// - public class ProfilingViewEngineWrapperMvcViewOptionsSetup : IConfigureOptions + /// The + public ProfilingViewEngineWrapperMvcViewOptionsSetup(IProfiler profiler) => + _profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); + + /// + public void Configure(MvcViewOptions options) { - private readonly IProfiler _profiler; - - /// - /// Initializes a new instance of the class. - /// - /// The - public ProfilingViewEngineWrapperMvcViewOptionsSetup(IProfiler profiler) => _profiler = profiler ?? throw new ArgumentNullException(nameof(profiler)); - - /// - public void Configure(MvcViewOptions options) + if (options == null) { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - WrapViewEngines(options.ViewEngines); + throw new ArgumentNullException(nameof(options)); } - private void WrapViewEngines(IList viewEngines) - { - if (viewEngines == null || viewEngines.Count == 0) - { - return; - } + WrapViewEngines(options.ViewEngines); + } - var originalEngines = viewEngines.ToList(); - viewEngines.Clear(); - foreach (IViewEngine engine in originalEngines) - { - IViewEngine wrappedEngine = engine is ProfilingViewEngine ? engine : new ProfilingViewEngine(engine, _profiler); - viewEngines.Add(wrappedEngine); - } + private void WrapViewEngines(IList viewEngines) + { + if (viewEngines.Count == 0) + { + return; + } + + var originalEngines = viewEngines.ToList(); + viewEngines.Clear(); + foreach (IViewEngine engine in originalEngines) + { + IViewEngine wrappedEngine = + engine is ProfilingViewEngine ? engine : new ProfilingViewEngine(engine, _profiler); + viewEngines.Add(wrappedEngine); } } } diff --git a/src/Umbraco.Web.Website/ViewEngines/RenderRazorViewEngineOptionsSetup.cs b/src/Umbraco.Web.Website/ViewEngines/RenderRazorViewEngineOptionsSetup.cs index 737e93a78a..f9c8ed0ae0 100644 --- a/src/Umbraco.Web.Website/ViewEngines/RenderRazorViewEngineOptionsSetup.cs +++ b/src/Umbraco.Web.Website/ViewEngines/RenderRazorViewEngineOptionsSetup.cs @@ -1,49 +1,46 @@ -using System; -using System.Collections.Generic; -using System.Linq; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.Options; -namespace Umbraco.Cms.Web.Website.ViewEngines -{ - /// - /// Configure view engine locations for front-end rendering - /// - public class RenderRazorViewEngineOptionsSetup : IConfigureOptions - { - /// - public void Configure(RazorViewEngineOptions options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } +namespace Umbraco.Cms.Web.Website.ViewEngines; - options.ViewLocationExpanders.Add(new ViewLocationExpander()); +/// +/// Configure view engine locations for front-end rendering +/// +public class RenderRazorViewEngineOptionsSetup : IConfigureOptions +{ + /// + public void Configure(RazorViewEngineOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); } - /// - /// Expands the default view locations - /// - private class ViewLocationExpander : IViewLocationExpander + options.ViewLocationExpanders.Add(new ViewLocationExpander()); + } + + /// + /// Expands the default view locations + /// + private class ViewLocationExpander : IViewLocationExpander + { + public IEnumerable ExpandViewLocations( + ViewLocationExpanderContext context, IEnumerable viewLocations) { - public IEnumerable ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable viewLocations) + string[] umbViewLocations = { - string[] umbViewLocations = new string[] - { - "/Views/{0}.cshtml", - "/Views/Shared/{0}.cshtml", - "/Views/Partials/{0}.cshtml", - "/Views/MacroPartials/{0}.cshtml", - }; + "/Views/{0}.cshtml", "/Views/Shared/{0}.cshtml", "/Views/Partials/{0}.cshtml", + "/Views/MacroPartials/{0}.cshtml", + }; - viewLocations = umbViewLocations.Concat(viewLocations); + viewLocations = umbViewLocations.Concat(viewLocations); - return viewLocations; - } + return viewLocations; + } - // not a dynamic expander - public void PopulateValues(ViewLocationExpanderContext context) { } + // not a dynamic expander + public void PopulateValues(ViewLocationExpanderContext context) + { } } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/RenderNoContentControllerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/RenderNoContentControllerTests.cs index dae3cfdb9e..4dc16aa84d 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/RenderNoContentControllerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Controllers/RenderNoContentControllerTests.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Tests.Common; @@ -22,8 +23,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Website.Controllers { var mockUmbracoContext = new Mock(); mockUmbracoContext.Setup(x => x.Content.HasContent()).Returns(true); - var mockIOHelper = new Mock(); - var controller = new RenderNoContentController(new TestUmbracoContextAccessor(mockUmbracoContext.Object), mockIOHelper.Object, new TestOptionsSnapshot(new GlobalSettings())); + var mockHostingEnvironment = new Mock(); + var controller = new RenderNoContentController(new TestUmbracoContextAccessor(mockUmbracoContext.Object), new TestOptionsSnapshot(new GlobalSettings()), mockHostingEnvironment.Object); var result = controller.Index() as RedirectResult; @@ -41,13 +42,17 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Website.Controllers mockUmbracoContext.Setup(x => x.Content.HasContent()).Returns(false); var mockIOHelper = new Mock(); mockIOHelper.Setup(x => x.ResolveUrl(It.Is(y => y == UmbracoPathSetting))).Returns(UmbracoPath); + var mockHostingEnvironment = new Mock(); + mockHostingEnvironment.Setup(x => x.ToAbsolute(It.Is(y => y == UmbracoPathSetting))) + .Returns(UmbracoPath); + var globalSettings = new TestOptionsSnapshot(new GlobalSettings() { UmbracoPath = UmbracoPathSetting, NoNodesViewPath = ViewPath, }); - var controller = new RenderNoContentController(new TestUmbracoContextAccessor(mockUmbracoContext.Object), mockIOHelper.Object, globalSettings); + var controller = new RenderNoContentController(new TestUmbracoContextAccessor(mockUmbracoContext.Object), globalSettings, mockHostingEnvironment.Object); var result = controller.Index() as ViewResult; Assert.IsNotNull(result);