V10: Build warnings in Web.Website (#12332)

* add new rule to globalconfig

* Fix warnings in Web.Website

* Fix more warnings in Web.Website

* Fix more build warnings in Web.Website

* Fix more warnings in Web.Website

* Fix tests

* Fix proper constructor call

* Fix not being able to run project

* Fix Obsolete method

Co-authored-by: Nikolaj Geisle <niko737@edu.ucl.dk>
This commit is contained in:
Nikolaj Geisle
2022-05-06 15:06:39 +02:00
committed by GitHub
parent b648177a40
commit 4f3d680f06
47 changed files with 3525 additions and 3447 deletions

View File

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

View File

@@ -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<RenderNoContentController>(x => new RenderNoContentController(x.GetService<IUmbracoContextAccessor>()!, x.GetService<IOptionsSnapshot<GlobalSettings>>()!, x.GetService<IHostingEnvironment>()!));
return builder;
}
}

View File

@@ -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;
/// <summary>
/// Redirects to an Umbraco page by Id or Entity
/// </summary>
public class RedirectToUmbracoPageResult : IKeepTempDataResult
{
private readonly IPublishedUrlProvider _publishedUrlProvider;
private readonly QueryString _queryString;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private IPublishedContent? _publishedContent;
private string? _url;
/// <summary>
/// Redirects to an Umbraco page by Id or Entity
/// Initializes a new instance of the <see cref="RedirectToUmbracoPageResult" /> class.
/// </summary>
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;
}
/// <summary>
/// Initializes a new instance of the <see cref="RedirectToUmbracoPageResult"/> class.
/// </summary>
public RedirectToUmbracoPageResult(Guid key, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor)
{
Key = key;
_publishedUrlProvider = publishedUrlProvider;
_umbracoContextAccessor = umbracoContextAccessor;
}
/// <summary>
/// Initializes a new instance of the <see cref="RedirectToUmbracoPageResult" /> class.
/// </summary>
public RedirectToUmbracoPageResult(
Guid key,
QueryString queryString,
IPublishedUrlProvider publishedUrlProvider,
IUmbracoContextAccessor umbracoContextAccessor)
{
Key = key;
_queryString = queryString;
_publishedUrlProvider = publishedUrlProvider;
_umbracoContextAccessor = umbracoContextAccessor;
}
/// <summary>
/// Initializes a new instance of the <see cref="RedirectToUmbracoPageResult"/> class.
/// </summary>
public RedirectToUmbracoPageResult(Guid key, QueryString queryString, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor)
{
Key = key;
_queryString = queryString;
_publishedUrlProvider = publishedUrlProvider;
_umbracoContextAccessor = umbracoContextAccessor;
}
/// <summary>
/// Initializes a new instance of the <see cref="RedirectToUmbracoPageResult" /> class.
/// </summary>
public RedirectToUmbracoPageResult(
IPublishedContent? publishedContent,
IPublishedUrlProvider publishedUrlProvider,
IUmbracoContextAccessor umbracoContextAccessor)
{
_publishedContent = publishedContent;
Key = publishedContent?.Key ?? Guid.Empty;
_publishedUrlProvider = publishedUrlProvider;
_umbracoContextAccessor = umbracoContextAccessor;
}
/// <summary>
/// Initializes a new instance of the <see cref="RedirectToUmbracoPageResult"/> class.
/// </summary>
public RedirectToUmbracoPageResult(IPublishedContent? publishedContent, IPublishedUrlProvider publishedUrlProvider, IUmbracoContextAccessor umbracoContextAccessor)
{
_publishedContent = publishedContent;
Key = publishedContent?.Key ?? Guid.Empty;
_publishedUrlProvider = publishedUrlProvider;
_umbracoContextAccessor = umbracoContextAccessor;
}
/// <summary>
/// Initializes a new instance of the <see cref="RedirectToUmbracoPageResult" /> class.
/// </summary>
public RedirectToUmbracoPageResult(
IPublishedContent? publishedContent,
QueryString queryString,
IPublishedUrlProvider publishedUrlProvider,
IUmbracoContextAccessor umbracoContextAccessor)
{
_publishedContent = publishedContent;
Key = publishedContent?.Key ?? Guid.Empty;
_queryString = queryString;
_publishedUrlProvider = publishedUrlProvider;
_umbracoContextAccessor = umbracoContextAccessor;
}
/// <summary>
/// Initializes a new instance of the <see cref="RedirectToUmbracoPageResult"/> class.
/// </summary>
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;
}
}
/// <inheritdoc/>
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<IUrlHelperFactory>();
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;
}
}
/// <inheritdoc />
public Task ExecuteResultAsync(ActionContext context)
{
if (context is null)
{
throw new ArgumentNullException(nameof(context));
}
HttpContext httpContext = context.HttpContext;
IUrlHelperFactory urlHelperFactory = httpContext.RequestServices.GetRequiredService<IUrlHelperFactory>();
IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(context);
var destinationUrl = urlHelper.Content(Url);
if (_queryString.HasValue)
{
destinationUrl += _queryString.ToUriComponent();
}
httpContext.Response.Redirect(destinationUrl);
return Task.CompletedTask;
}
}

View File

@@ -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;
/// <summary>
/// Redirects to the current URL rendering an Umbraco page including it's query strings
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public class RedirectToUmbracoUrlResult : IKeepTempDataResult
{
private readonly IUmbracoContext _umbracoContext;
/// <summary>
/// Redirects to the current URL rendering an Umbraco page including it's query strings
/// Initializes a new instance of the <see cref="RedirectToUmbracoUrlResult" /> class.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public class RedirectToUmbracoUrlResult : IActionResult, IKeepTempDataResult
public RedirectToUmbracoUrlResult(IUmbracoContext umbracoContext) => _umbracoContext = umbracoContext;
/// <inheritdoc />
public Task ExecuteResultAsync(ActionContext context)
{
private readonly IUmbracoContext _umbracoContext;
/// <summary>
/// Initializes a new instance of the <see cref="RedirectToUmbracoUrlResult"/> class.
/// </summary>
public RedirectToUmbracoUrlResult(IUmbracoContext umbracoContext) => _umbracoContext = umbracoContext;
/// <inheritdoc/>
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;
}
}

View File

@@ -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;
/// <summary>
/// Used by posted forms to proxy the result to the page in which the current URL matches on
/// </summary>
/// <remarks>
/// This page does not redirect therefore it does not implement <see cref="IKeepTempDataResult" /> 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.
/// </remarks>
public class UmbracoPageResult : IActionResult
{
private readonly IProfilingLogger _profilingLogger;
/// <summary>
/// Used by posted forms to proxy the result to the page in which the current URL matches on
/// Initializes a new instance of the <see cref="UmbracoPageResult" /> class.
/// </summary>
/// <remarks>
/// This page does not redirect therefore it does not implement <see cref="IKeepTempDataResult"/> 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.
/// </remarks>
public class UmbracoPageResult : IActionResult
public UmbracoPageResult(IProfilingLogger profilingLogger) => _profilingLogger = profilingLogger;
/// <inheritdoc />
public async Task ExecuteResultAsync(ActionContext context)
{
private readonly IProfilingLogger _profilingLogger;
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoPageResult"/> class.
/// </summary>
public UmbracoPageResult(IProfilingLogger profilingLogger) => _profilingLogger = profilingLogger;
/// <inheritdoc/>
public async Task ExecuteResultAsync(ActionContext context)
UmbracoRouteValues? umbracoRouteValues = context.HttpContext.Features.Get<UmbracoRouteValues>();
if (umbracoRouteValues == null)
{
UmbracoRouteValues? umbracoRouteValues = context.HttpContext.Features.Get<UmbracoRouteValues>();
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<IActionInvokerFactory>();
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");
}
/// <summary>
/// Executes the controller action
/// </summary>
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<UmbracoPageResult>("Executing Umbraco RouteDefinition controller", "Finished"))
ActionDescriptor = umbracoRouteValues.ControllerActionDescriptor,
};
IActionInvokerFactory actionInvokerFactory =
context.HttpContext.RequestServices.GetRequiredService<IActionInvokerFactory>();
IActionInvoker? actionInvoker = actionInvokerFactory.CreateInvoker(renderActionContext);
await ExecuteControllerAction(actionInvoker);
}
/// <summary>
/// Executes the controller action
/// </summary>
private async Task ExecuteControllerAction(IActionInvoker? actionInvoker)
{
using (_profilingLogger.TraceDuration<UmbracoPageResult>(
"Executing Umbraco RouteDefinition controller",
"Finished"))
{
if (actionInvoker is not null)
{
if (actionInvoker is not null)
{
await actionInvoker.InvokeAsync();
}
await actionInvoker.InvokeAsync();
}
}
}

View File

@@ -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<Type>
{
public class SurfaceControllerTypeCollection : BuilderCollectionBase<Type>
public SurfaceControllerTypeCollection(Func<IEnumerable<Type>> items)
: base(items)
{
public SurfaceControllerTypeCollection(Func<IEnumerable<Type>> items) : base(items)
{
}
}
}

View File

@@ -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<SurfaceControllerTypeCollectionBuilder,
SurfaceControllerTypeCollection, SurfaceController>
{
public class SurfaceControllerTypeCollectionBuilder : TypeCollectionBuilderBase<SurfaceControllerTypeCollectionBuilder, SurfaceControllerTypeCollection, SurfaceController>
{
protected override SurfaceControllerTypeCollectionBuilder This => this;
}
protected override SurfaceControllerTypeCollectionBuilder This => this;
}

View File

@@ -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> globalSettings)
: this(umbracoContextAccessor, globalSettings, StaticServiceProvider.Instance.GetRequiredService<IHostingEnvironment>())
{
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IIOHelper _ioHelper;
private readonly GlobalSettings _globalSettings;
}
public RenderNoContentController(IUmbracoContextAccessor umbracoContextAccessor, IIOHelper ioHelper, IOptionsSnapshot<GlobalSettings> globalSettings)
[ActivatorUtilitiesConstructor]
public RenderNoContentController(
IUmbracoContextAccessor umbracoContextAccessor,
IOptionsSnapshot<GlobalSettings> 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);
}
}

View File

@@ -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;
/// <summary>
/// Provides a base class for front-end add-in controllers.
/// </summary>
[AutoValidateAntiforgeryToken]
public abstract class SurfaceController : PluginController
{
/// <summary>
/// Provides a base class for front-end add-in controllers.
/// Initializes a new instance of the <see cref="SurfaceController" /> class.
/// </summary>
[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; }
/// <summary>
/// Gets the current page.
/// </summary>
protected virtual IPublishedContent? CurrentPage
{
/// <summary>
/// Initializes a new instance of the <see cref="SurfaceController"/> class.
/// </summary>
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; }
/// <summary>
/// Gets the current page.
/// </summary>
protected virtual IPublishedContent? CurrentPage
get
{
get
UmbracoRouteValues? umbracoRouteValues = HttpContext.Features.Get<UmbracoRouteValues>();
if (umbracoRouteValues is null)
{
UmbracoRouteValues? umbracoRouteValues = HttpContext.Features.Get<UmbracoRouteValues>();
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");
}
}
/// <summary>
/// Redirects to the Umbraco page with the given id
/// </summary>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid contentKey)
=> new RedirectToUmbracoPageResult(contentKey, PublishedUrlProvider, UmbracoContextAccessor);
/// <summary>
/// Redirects to the Umbraco page with the given id and passes provided querystring
/// </summary>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid contentKey, QueryString queryString)
=> new RedirectToUmbracoPageResult(contentKey, queryString, PublishedUrlProvider, UmbracoContextAccessor);
/// <summary>
/// Redirects to the Umbraco page with the given published content
/// </summary>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent)
=> new RedirectToUmbracoPageResult(publishedContent, PublishedUrlProvider, UmbracoContextAccessor);
/// <summary>
/// Redirects to the Umbraco page with the given published content and passes provided querystring
/// </summary>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent, QueryString queryString)
=> new RedirectToUmbracoPageResult(publishedContent, queryString, PublishedUrlProvider, UmbracoContextAccessor);
/// <summary>
/// Redirects to the currently rendered Umbraco page
/// </summary>
protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage()
=> new RedirectToUmbracoPageResult(CurrentPage, PublishedUrlProvider, UmbracoContextAccessor);
/// <summary>
/// Redirects to the currently rendered Umbraco page and passes provided querystring
/// </summary>
protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage(QueryString queryString)
=> new RedirectToUmbracoPageResult(CurrentPage, queryString, PublishedUrlProvider, UmbracoContextAccessor);
/// <summary>
/// Redirects to the currently rendered Umbraco URL
/// </summary>
/// <remarks>
/// 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.*
/// </remarks>
protected RedirectToUmbracoUrlResult RedirectToCurrentUmbracoUrl()
=> new RedirectToUmbracoUrlResult(UmbracoContext);
/// <summary>
/// Returns the currently rendered Umbraco page
/// </summary>
protected UmbracoPageResult CurrentUmbracoPage()
{
HttpContext.Features.Set(new ProxyViewDataFeature(ViewData, TempData));
return new UmbracoPageResult(ProfilingLogger);
return umbracoRouteValues.PublishedRequest.PublishedContent;
}
}
/// <summary>
/// Redirects to the Umbraco page with the given id
/// </summary>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid contentKey)
=> new(contentKey, PublishedUrlProvider, UmbracoContextAccessor);
/// <summary>
/// Redirects to the Umbraco page with the given id and passes provided querystring
/// </summary>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(Guid contentKey, QueryString queryString)
=> new(contentKey, queryString, PublishedUrlProvider, UmbracoContextAccessor);
/// <summary>
/// Redirects to the Umbraco page with the given published content
/// </summary>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(IPublishedContent publishedContent)
=> new(publishedContent, PublishedUrlProvider, UmbracoContextAccessor);
/// <summary>
/// Redirects to the Umbraco page with the given published content and passes provided querystring
/// </summary>
protected RedirectToUmbracoPageResult RedirectToUmbracoPage(
IPublishedContent publishedContent,
QueryString queryString)
=> new(publishedContent, queryString, PublishedUrlProvider, UmbracoContextAccessor);
/// <summary>
/// Redirects to the currently rendered Umbraco page
/// </summary>
protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage()
=> new(CurrentPage, PublishedUrlProvider, UmbracoContextAccessor);
/// <summary>
/// Redirects to the currently rendered Umbraco page and passes provided querystring
/// </summary>
protected RedirectToUmbracoPageResult RedirectToCurrentUmbracoPage(QueryString queryString)
=> new(CurrentPage, queryString, PublishedUrlProvider, UmbracoContextAccessor);
/// <summary>
/// Redirects to the currently rendered Umbraco URL
/// </summary>
/// <remarks>
/// 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.*
/// </remarks>
protected RedirectToUmbracoUrlResult RedirectToCurrentUmbracoUrl()
=> new(UmbracoContext);
/// <summary>
/// Returns the currently rendered Umbraco page
/// </summary>
protected UmbracoPageResult CurrentUmbracoPage()
{
HttpContext.Features.Set(new ProxyViewDataFeature(ViewData, TempData));
return new UmbracoPageResult(ProfilingLogger);
}
}

View File

@@ -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<UmbExternalLoginController> _logger;
private readonly IMemberManager _memberManager;
private readonly IMemberSignInManager _memberSignInManager;
private readonly IOptions<SecuritySettings> _securitySettings;
private readonly ITwoFactorLoginService _twoFactorLoginService;
public UmbExternalLoginController(
ILogger<UmbExternalLoginController> logger,
IUmbracoContextAccessor umbracoContextAccessor,
IUmbracoDatabaseFactory databaseFactory,
ServiceContext services,
AppCaches appCaches,
IProfilingLogger profilingLogger,
IPublishedUrlProvider publishedUrlProvider,
IMemberSignInManager memberSignInManager,
IMemberManager memberManager,
ITwoFactorLoginService twoFactorLoginService,
IOptions<SecuritySettings> securitySettings)
: base(
umbracoContextAccessor,
databaseFactory,
services,
appCaches,
profilingLogger,
publishedUrlProvider)
{
private readonly IMemberManager _memberManager;
private readonly ITwoFactorLoginService _twoFactorLoginService;
private readonly IOptions<SecuritySettings> _securitySettings;
private readonly ILogger<UmbExternalLoginController> _logger;
private readonly IMemberSignInManager _memberSignInManager;
_logger = logger;
_memberSignInManager = memberSignInManager;
_memberManager = memberManager;
_twoFactorLoginService = twoFactorLoginService;
_securitySettings = securitySettings;
}
public UmbExternalLoginController(
ILogger<UmbExternalLoginController> logger,
IUmbracoContextAccessor umbracoContextAccessor,
IUmbracoDatabaseFactory databaseFactory,
ServiceContext services,
AppCaches appCaches,
IProfilingLogger profilingLogger,
IPublishedUrlProvider publishedUrlProvider,
IMemberSignInManager memberSignInManager,
IMemberManager memberManager,
ITwoFactorLoginService twoFactorLoginService,
IOptions<SecuritySettings> securitySettings)
: base(
umbracoContextAccessor,
databaseFactory,
services,
appCaches,
profilingLogger,
publishedUrlProvider)
/// <summary>
/// Endpoint used to redirect to a specific login provider. This endpoint is used from the Login Macro snippet.
/// </summary>
[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();
}
/// <summary>
/// Endpoint used to redirect to a specific login provider. This endpoint is used from the Login Macro snippet.
/// </summary>
[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);
}
/// <summary>
/// Endpoint used my the login provider to call back to our solution.
/// </summary>
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl)
{
var errors = new List<string>();
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");
}
/// <summary>
/// Endpoint used my the login provider to call back to our solution.
/// </summary>
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl)
else
{
var errors = new List<string>();
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<string> 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<IActionResult> 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<string>();
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<IActionResult> ExternalLinkLoginCallback(string returnUrl)
{
MemberIdentityUser user = await _memberManager.GetUserAsync(User);
string? loginProvider = null;
var errors = new List<string>();
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<IActionResult> Disassociate(string provider, string providerKey, string? returnUrl = null)
private IActionResult RedirectToLocal(string returnUrl) =>
Url.IsLocalUrl(returnUrl) ? Redirect(returnUrl) : RedirectToCurrentUmbracoPage();
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> 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();
}
}

View File

@@ -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<IMemberManager>(),
StaticServiceProvider.Instance.GetRequiredService<ITwoFactorLoginService>())
{
}
[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<IActionResult> 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<IMemberManager>(),
StaticServiceProvider.Instance.GetRequiredService<ITwoFactorLoginService>())
{
}
[HttpPost]
[ValidateAntiForgeryToken]
[ValidateUmbracoFormRouteString]
public async Task<IActionResult> 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();
}
/// <summary>
/// We pass in values via encrypted route values so they cannot be tampered with and merge them into the model for use
/// </summary>
/// <param name="model"></param>
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<string> 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();
}
/// <summary>
/// We pass in values via encrypted route values so they cannot be tampered with and merge them into the model for use
/// </summary>
/// <param name="model"></param>
private void MergeRouteValuesToModel(LoginModel model)
{
if (RouteData.Values.TryGetValue(nameof(LoginModel.RedirectUrl), out var redirectUrl) && redirectUrl != null)
{
model.RedirectUrl = redirectUrl.ToString();
}
}
}

View File

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

View File

@@ -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<IActionResult> 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<IActionResult> 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();
}
/// <summary>
/// We pass in values via encrypted route values so they cannot be tampered with and merge them into the model for use
/// </summary>
/// <param name="model"></param>
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<IdentityResult> UpdateMemberAsync(ProfileModel model, MemberIdentityUser currentMember)
// Redirect to current page by default.
return RedirectToCurrentUmbracoPage();
}
/// <summary>
/// We pass in values via encrypted route values so they cannot be tampered with and merge them into the model for use
/// </summary>
/// <param name="model"></param>
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<IdentityResult> 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;
}
}

View File

@@ -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<IActionResult> 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<IActionResult> 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();
}
/// <summary>
/// We pass in values via encrypted route values so they cannot be tampered with and merge them into the model for use
/// </summary>
/// <param name="model"></param>
private void MergeRouteValuesToModel(RegisterModel model)
AddErrors(result);
return CurrentUmbracoPage();
}
/// <summary>
/// We pass in values via encrypted route values so they cannot be tampered with and merge them into the model for use
/// </summary>
/// <param name="model"></param>
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()!;
}
/// <summary>
/// Registers a new member.
/// </summary>
/// <param name="model">Register member model.</param>
/// <param name="logMemberIn">Flag for whether to log the member in upon successful registration.</param>
/// <returns>Result of registration operation.</returns>
private async Task<IdentityResult> 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);
}
}
/// <summary>
/// Registers a new member.
/// </summary>
/// <param name="model">Register member model.</param>
/// <param name="logMemberIn">Flag for whether to log the member in upon successful registration.</param>
/// <returns>Result of registration operation.</returns>
private async Task<IdentityResult> 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;
}
}

View File

@@ -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<UmbTwoFactorLoginController> _logger;
private readonly IMemberManager _memberManager;
private readonly IMemberSignInManager _memberSignInManager;
private readonly ITwoFactorLoginService _twoFactorLoginService;
public UmbTwoFactorLoginController(
ILogger<UmbTwoFactorLoginController> 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<UmbTwoFactorLoginController> _logger;
private readonly IMemberSignInManager _memberSignInManager;
_logger = logger;
_memberSignInManager = memberSignInManager;
_memberManager = memberManager;
_twoFactorLoginService = twoFactorLoginService;
}
public UmbTwoFactorLoginController(
ILogger<UmbTwoFactorLoginController> 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)
/// <summary>
/// Used to retrieve the 2FA providers for code submission
/// </summary>
/// <returns></returns>
[AllowAnonymous]
public async Task<ActionResult<IEnumerable<string>>> 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();
}
/// <summary>
/// Used to retrieve the 2FA providers for code submission
/// </summary>
/// <returns></returns>
[AllowAnonymous]
public async Task<ActionResult<IEnumerable<string>>> Get2FAProviders()
{
var user = await _memberSignInManager.GetTwoFactorAuthenticationUserAsync();
if (user == null)
{
_logger.LogWarning("Get2FAProviders :: No verified member found, returning 404");
return NotFound();
}
IList<string> userFactors = await _memberManager.GetValidTwoFactorProvidersAsync(user);
return new ObjectResult(userFactors);
}
var userFactors = await _memberManager.GetValidTwoFactorProvidersAsync(user);
return new ObjectResult(userFactors);
[AllowAnonymous]
public async Task<IActionResult> 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<IActionResult> 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<string> providerNames =
await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(user.Key);
ViewData.SetTwoFactorProviderNames(providerNames);
return CurrentUmbracoPage();
}
[HttpPost]
public async Task<IActionResult> 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<IActionResult> 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<IActionResult> 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<IActionResult> 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();
}

View File

@@ -1,16 +1,14 @@
using System;
using Umbraco.Cms.Web.Common.Controllers;
namespace Umbraco.Cms.Web.Website.Controllers
namespace Umbraco.Cms.Web.Website.Controllers;
/// <summary>
/// The defaults used for rendering Umbraco front-end pages
/// </summary>
public class UmbracoRenderingDefaultsOptions
{
/// <summary>
/// The defaults used for rendering Umbraco front-end pages
/// Gets the default umbraco render controller type
/// </summary>
public class UmbracoRenderingDefaultsOptions
{
/// <summary>
/// Gets the default umbraco render controller type
/// </summary>
public Type DefaultControllerType { get; set; } = typeof(RenderController);
}
public Type DefaultControllerType { get; set; } = typeof(RenderController);
}

View File

@@ -1,23 +1,21 @@
using System;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Web.Website.Security;
namespace Umbraco.Extensions;
namespace Umbraco.Extensions
/// <summary>
/// Extension methods for <see cref="IUmbracoBuilder" /> for the Umbraco back office
/// </summary>
public static partial class UmbracoBuilderExtensions
{
/// <summary>
/// Extension methods for <see cref="IUmbracoBuilder"/> for the Umbraco back office
/// Adds support for external login providers in Umbraco
/// </summary>
public static partial class UmbracoBuilderExtensions
public static IUmbracoBuilder AddMemberExternalLogins(
this IUmbracoBuilder umbracoBuilder,
Action<MemberExternalLoginsBuilder> builder)
{
/// <summary>
/// Adds support for external login providers in Umbraco
/// </summary>
public static IUmbracoBuilder AddMemberExternalLogins(this IUmbracoBuilder umbracoBuilder, Action<MemberExternalLoginsBuilder> builder)
{
builder(new MemberExternalLoginsBuilder(umbracoBuilder.Services));
return umbracoBuilder;
}
builder(new MemberExternalLoginsBuilder(umbracoBuilder.Services));
return umbracoBuilder;
}
}

View File

@@ -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;
/// <summary>
/// <see cref="IUmbracoBuilder" /> extensions for umbraco front-end website
/// </summary>
public static partial class UmbracoBuilderExtensions
{
/// <summary>
/// <see cref="IUmbracoBuilder"/> extensions for umbraco front-end website
/// Add services for the umbraco front-end website
/// </summary>
public static partial class UmbracoBuilderExtensions
public static IUmbracoBuilder AddWebsite(this IUmbracoBuilder builder)
{
/// <summary>
/// Add services for the umbraco front-end website
/// </summary>
public static IUmbracoBuilder AddWebsite(this IUmbracoBuilder builder)
{
builder.WithCollectionBuilder<SurfaceControllerTypeCollectionBuilder>()?
.Add(builder.TypeLoader.GetSurfaceControllers());
builder.WithCollectionBuilder<SurfaceControllerTypeCollectionBuilder>()?
.Add(builder.TypeLoader.GetSurfaceControllers());
// Configure MVC startup options for custom view locations
builder.Services.ConfigureOptions<RenderRazorViewEngineOptionsSetup>();
builder.Services.ConfigureOptions<PluginRazorViewEngineOptionsSetup>();
// Configure MVC startup options for custom view locations
builder.Services.ConfigureOptions<RenderRazorViewEngineOptionsSetup>();
builder.Services.ConfigureOptions<PluginRazorViewEngineOptionsSetup>();
// Wraps all existing view engines in a ProfilerViewEngine
builder.Services.AddTransient<IConfigureOptions<MvcViewOptions>, ProfilingViewEngineWrapperMvcViewOptionsSetup>();
// Wraps all existing view engines in a ProfilerViewEngine
builder.Services
.AddTransient<IConfigureOptions<MvcViewOptions>, 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<UmbracoRouteValueTransformer>();
builder.Services.AddSingleton<IControllerActionSearcher, ControllerActionSearcher>();
builder.Services.TryAddEnumerable(Singleton<MatcherPolicy, NotFoundSelectorPolicy>());
builder.Services.AddSingleton<IUmbracoRouteValuesFactory, UmbracoRouteValuesFactory>();
builder.Services.AddSingleton<IRoutableDocumentFilter, RoutableDocumentFilter>();
builder.Services.AddSingleton<UmbracoRouteValueTransformer>();
builder.Services.AddSingleton<IControllerActionSearcher, ControllerActionSearcher>();
builder.Services.TryAddEnumerable(Singleton<MatcherPolicy, NotFoundSelectorPolicy>());
builder.Services.AddSingleton<IUmbracoRouteValuesFactory, UmbracoRouteValuesFactory>();
builder.Services.AddSingleton<IRoutableDocumentFilter, RoutableDocumentFilter>();
builder.Services.AddSingleton<FrontEndRoutes>();
builder.Services.AddSingleton<FrontEndRoutes>();
builder.Services.AddSingleton<MemberModelBuilderFactory>();
builder.Services.AddSingleton<MemberModelBuilderFactory>();
builder.Services.AddSingleton<IPublicAccessRequestHandler, PublicAccessRequestHandler>();
builder.Services.AddSingleton<BasicAuthenticationMiddleware>();
builder.Services.AddSingleton<IPublicAccessRequestHandler, PublicAccessRequestHandler>();
builder.Services.AddSingleton<BasicAuthenticationMiddleware>();
builder
.AddDistributedCache()
.AddModelsBuilder();
builder
.AddDistributedCache()
.AddModelsBuilder();
builder.AddMembersIdentity();
builder.AddMembersIdentity();
return builder;
}
return builder;
}
}

View File

@@ -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
/// <summary>
/// Return the Url for a Surface Controller
/// </summary>
/// <typeparam name="T">The <see cref="SurfaceController" /></typeparam>
public static string? GetUmbracoSurfaceUrl<T>(
this LinkGenerator linkGenerator,
Expression<Func<T, object>> methodSelector)
where T : SurfaceController
{
/// <summary>
/// Return the Url for a Surface Controller
/// </summary>
/// <typeparam name="T">The <see cref="SurfaceController"/></typeparam>
public static string? GetUmbracoSurfaceUrl<T>(this LinkGenerator linkGenerator, Expression<Func<T, object>> methodSelector)
where T : SurfaceController
MethodInfo? method = ExpressionHelper.GetMethodInfo(methodSelector);
IDictionary<string, object?>? methodParams = ExpressionHelper.GetMethodParams(methodSelector);
if (method == null)
{
MethodInfo? method = ExpressionHelper.GetMethodInfo(methodSelector);
IDictionary<string, object?>? 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<T>(method.Name);
}
return linkGenerator.GetUmbracoSurfaceUrl<T>(method.Name, methodParams);
throw new MissingMethodException(
$"Could not find the method {methodSelector} on type {typeof(T)} or the result ");
}
/// <summary>
/// Return the Url for a Surface Controller
/// </summary>
/// <typeparam name="T">The <see cref="SurfaceController"/></typeparam>
public static string? GetUmbracoSurfaceUrl<T>(this LinkGenerator linkGenerator, string actionName, object? id = null)
where T : SurfaceController => linkGenerator.GetUmbracoControllerUrl(
actionName,
typeof(T),
new Dictionary<string, object?>()
{
["id"] = id
});
if (methodParams is null || methodParams.Any() == false)
{
return linkGenerator.GetUmbracoSurfaceUrl<T>(method.Name);
}
return linkGenerator.GetUmbracoSurfaceUrl<T>(method.Name, methodParams);
}
/// <summary>
/// Return the Url for a Surface Controller
/// </summary>
/// <typeparam name="T">The <see cref="SurfaceController" /></typeparam>
public static string? GetUmbracoSurfaceUrl<T>(this LinkGenerator linkGenerator, string actionName, object? id = null)
where T : SurfaceController => linkGenerator.GetUmbracoControllerUrl(
actionName,
typeof(T),
new Dictionary<string, object?> { ["id"] = id });
}

View File

@@ -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;
/// <summary>
/// Provides extension methods for the <see cref="TypeLoader" /> class.
/// </summary>
// Migrated to .NET Core
public static class TypeLoaderExtensions
{
/// <summary>
/// Provides extension methods for the <see cref="TypeLoader"/> class.
/// Gets all types implementing <see cref="SurfaceController" />.
/// </summary>
// Migrated to .NET Core
public static class TypeLoaderExtensions
{
/// <summary>
/// Gets all types implementing <see cref="SurfaceController"/>.
/// </summary>
internal static IEnumerable<Type> GetSurfaceControllers(this TypeLoader typeLoader)
=> typeLoader.GetTypes<SurfaceController>();
internal static IEnumerable<Type> GetSurfaceControllers(this TypeLoader typeLoader)
=> typeLoader.GetTypes<SurfaceController>();
/// <summary>
/// Gets all types implementing <see cref="UmbracoApiController"/>.
/// </summary>
internal static IEnumerable<Type> GetUmbracoApiControllers(this TypeLoader typeLoader)
=> typeLoader.GetTypes<UmbracoApiController>();
}
/// <summary>
/// Gets all types implementing <see cref="UmbracoApiController" />.
/// </summary>
internal static IEnumerable<Type> GetUmbracoApiControllers(this TypeLoader typeLoader)
=> typeLoader.GetTypes<UmbracoApiController>();
}

View File

@@ -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;
/// <summary>
/// <see cref="IApplicationBuilder" /> extensions for the umbraco front-end website
/// </summary>
public static class UmbracoApplicationBuilderExtensions
{
/// <summary>
/// <see cref="IApplicationBuilder"/> extensions for the umbraco front-end website
/// Adds all required middleware to run the website
/// </summary>
public static partial class UmbracoApplicationBuilderExtensions
/// <param name="builder"></param>
/// <returns></returns>
public static IUmbracoApplicationBuilderContext UseWebsite(this IUmbracoApplicationBuilderContext builder)
{
/// <summary>
/// Adds all required middleware to run the website
/// </summary>
/// <param name="builder"></param>
/// <returns></returns>
public static IUmbracoApplicationBuilderContext UseWebsite(this IUmbracoApplicationBuilderContext builder)
builder.AppBuilder.UseMiddleware<BasicAuthenticationMiddleware>();
return builder;
}
/// <summary>
/// Sets up routes for the front-end umbraco website
/// </summary>
public static IUmbracoEndpointBuilderContext UseWebsiteEndpoints(this IUmbracoEndpointBuilderContext builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (!builder.RuntimeState.UmbracoCanBoot())
{
builder.AppBuilder.UseMiddleware<BasicAuthenticationMiddleware>();
return builder;
}
/// <summary>
/// Sets up routes for the front-end umbraco website
/// </summary>
public static IUmbracoEndpointBuilderContext UseWebsiteEndpoints(this IUmbracoEndpointBuilderContext builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
FrontEndRoutes surfaceRoutes = builder.ApplicationServices.GetRequiredService<FrontEndRoutes>();
surfaceRoutes.CreateRoutes(builder.EndpointRouteBuilder);
builder.EndpointRouteBuilder.MapDynamicControllerRoute<UmbracoRouteValueTransformer>("/{**slug}");
if (!builder.RuntimeState.UmbracoCanBoot())
{
return builder;
}
FrontEndRoutes surfaceRoutes = builder.ApplicationServices.GetRequiredService<FrontEndRoutes>();
surfaceRoutes.CreateRoutes(builder.EndpointRouteBuilder);
builder.EndpointRouteBuilder.MapDynamicControllerRoute<UmbracoRouteValueTransformer>("/{**slug}");
return builder;
}
return builder;
}
}

View File

@@ -1,84 +1,88 @@
using System;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Routing;
namespace Umbraco.Extensions
namespace Umbraco.Extensions;
/// <summary>
/// Provides extension methods to the <see cref="IUmbracoBuilder" /> class.
/// </summary>
public static class WebsiteUmbracoBuilderExtensions
{
#region Uniques
/// <summary>
/// Provides extension methods to the <see cref="IUmbracoBuilder"/> class.
/// Sets the content last chance finder.
/// </summary>
public static class WebsiteUmbracoBuilderExtensions
/// <typeparam name="T">The type of the content last chance finder.</typeparam>
/// <param name="builder">The builder.</param>
public static IUmbracoBuilder SetContentLastChanceFinder<T>(this IUmbracoBuilder builder)
where T : class, IContentLastChanceFinder
{
#region Uniques
/// <summary>
/// Sets the content last chance finder.
/// </summary>
/// <typeparam name="T">The type of the content last chance finder.</typeparam>
/// <param name="builder">The builder.</param>
public static IUmbracoBuilder SetContentLastChanceFinder<T>(this IUmbracoBuilder builder)
where T : class, IContentLastChanceFinder
{
builder.Services.AddUnique<IContentLastChanceFinder, T>();
return builder;
}
/// <summary>
/// Sets the content last chance finder.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="factory">A function creating a last chance finder.</param>
public static IUmbracoBuilder SetContentLastChanceFinder(this IUmbracoBuilder builder, Func<IServiceProvider, IContentLastChanceFinder> factory)
{
builder.Services.AddUnique(factory);
return builder;
}
/// <summary>
/// Sets the content last chance finder.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="finder">A last chance finder.</param>
public static IUmbracoBuilder SetContentLastChanceFinder(this IUmbracoBuilder builder, IContentLastChanceFinder finder)
{
builder.Services.AddUnique(finder);
return builder;
}
/// <summary>
/// Sets the site domain helper.
/// </summary>
/// <typeparam name="T">The type of the site domain helper.</typeparam>
/// <param name="builder"></param>
public static IUmbracoBuilder SetSiteDomainHelper<T>(this IUmbracoBuilder builder)
where T : class, ISiteDomainMapper
{
builder.Services.AddUnique<ISiteDomainMapper, T>();
return builder;
}
/// <summary>
/// Sets the site domain helper.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="factory">A function creating a helper.</param>
public static IUmbracoBuilder SetSiteDomainHelper(this IUmbracoBuilder builder, Func<IServiceProvider, ISiteDomainMapper> factory)
{
builder.Services.AddUnique(factory);
return builder;
}
/// <summary>
/// Sets the site domain helper.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="helper">A helper.</param>
public static IUmbracoBuilder SetSiteDomainHelper(this IUmbracoBuilder builder, ISiteDomainMapper helper)
{
builder.Services.AddUnique(helper);
return builder;
}
#endregion
builder.Services.AddUnique<IContentLastChanceFinder, T>();
return builder;
}
/// <summary>
/// Sets the content last chance finder.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="factory">A function creating a last chance finder.</param>
public static IUmbracoBuilder SetContentLastChanceFinder(
this IUmbracoBuilder builder,
Func<IServiceProvider, IContentLastChanceFinder> factory)
{
builder.Services.AddUnique(factory);
return builder;
}
/// <summary>
/// Sets the content last chance finder.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="finder">A last chance finder.</param>
public static IUmbracoBuilder SetContentLastChanceFinder(
this IUmbracoBuilder builder,
IContentLastChanceFinder finder)
{
builder.Services.AddUnique(finder);
return builder;
}
/// <summary>
/// Sets the site domain helper.
/// </summary>
/// <typeparam name="T">The type of the site domain helper.</typeparam>
/// <param name="builder"></param>
public static IUmbracoBuilder SetSiteDomainHelper<T>(this IUmbracoBuilder builder)
where T : class, ISiteDomainMapper
{
builder.Services.AddUnique<ISiteDomainMapper, T>();
return builder;
}
/// <summary>
/// Sets the site domain helper.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="factory">A function creating a helper.</param>
public static IUmbracoBuilder SetSiteDomainHelper(
this IUmbracoBuilder builder,
Func<IServiceProvider, ISiteDomainMapper> factory)
{
builder.Services.AddUnique(factory);
return builder;
}
/// <summary>
/// Sets the site domain helper.
/// </summary>
/// <param name="builder">The builder.</param>
/// <param name="helper">A helper.</param>
public static IUmbracoBuilder SetSiteDomainHelper(this IUmbracoBuilder builder, ISiteDomainMapper helper)
{
builder.Services.AddUnique(helper);
return builder;
}
#endregion
}

View File

@@ -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
{
/// <summary>
/// Provides basic authentication via back-office credentials for public website access if configured for use and the client IP is not allow listed.
/// </summary>
public class BasicAuthenticationMiddleware : IMiddleware
{
private readonly IRuntimeState _runtimeState;
private readonly IBasicAuthService _basicAuthService;
namespace Umbraco.Cms.Web.Common.Middleware;
public BasicAuthenticationMiddleware(
IRuntimeState runtimeState,
IBasicAuthService basicAuthService)
/// <summary>
/// Provides basic authentication via back-office credentials for public website access if configured for use and the
/// client IP is not allow listed.
/// </summary>
public class BasicAuthenticationMiddleware : IMiddleware
{
private readonly IBasicAuthService _basicAuthService;
private readonly IRuntimeState _runtimeState;
public BasicAuthenticationMiddleware(
IRuntimeState runtimeState,
IBasicAuthService basicAuthService)
{
_runtimeState = runtimeState;
_basicAuthService = basicAuthService;
}
/// <inheritdoc />
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;
}
/// <inheritdoc />
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<IBackOfficeSignInManager>();
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<IBackOfficeSignInManager>();
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\"");
}
}

View File

@@ -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<MemberPropertyModel> GetMemberPropertiesViewModel(IMemberType memberType, IMember? member = null)
{
var viewProperties = new List<MemberPropertyModel>();
var builtIns = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper).Select(x => x.Key).ToArray();
IOrderedEnumerable<IPropertyType> 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<MemberPropertyModel> GetMemberPropertiesViewModel(IMemberType memberType, IMember? member = null)
{
var viewProperties = new List<MemberPropertyModel>();
var builtIns = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper).Select(x => x.Key).ToArray();
IOrderedEnumerable<IPropertyType> 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;
}
}

View File

@@ -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;
/// <summary>
/// Service to create model builder instances for working with Members on the front-end
/// </summary>
public class MemberModelBuilderFactory
{
/// <summary>
/// Service to create model builder instances for working with Members on the front-end
/// </summary>
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;
}
/// <summary>
/// Create a <see cref="RegisterModelBuilder"/>
/// </summary>
/// <returns></returns>
public RegisterModelBuilder CreateRegisterModel() => new RegisterModelBuilder(_memberTypeService, _shortStringHelper);
/// <summary>
/// Create a <see cref="RegisterModelBuilder"/>
/// </summary>
/// <returns></returns>
public ProfileModelBuilder CreateProfileModel() => new ProfileModelBuilder(_memberTypeService, _memberService, _shortStringHelper, _httpContextAccessor);
_memberTypeService = memberTypeService;
_memberService = memberService;
_shortStringHelper = shortStringHelper;
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
/// Create a <see cref="RegisterModelBuilder" />
/// </summary>
/// <returns></returns>
public RegisterModelBuilder CreateRegisterModel() => new(_memberTypeService, _shortStringHelper);
/// <summary>
/// Create a <see cref="RegisterModelBuilder" />
/// </summary>
/// <returns></returns>
public ProfileModelBuilder CreateProfileModel() =>
new(_memberTypeService, _memberService, _shortStringHelper, _httpContextAccessor);
}

View File

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

View File

@@ -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;
/// <summary>
/// A readonly member profile model
/// </summary>
public class ProfileModel : PostRedirectModel
{
[ReadOnly(true)]
public Guid Key { get; set; }
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; } = null!;
/// <summary>
/// A readonly member profile model
/// The member's real name
/// </summary>
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; }
/// <summary>
/// The member's real name
/// </summary>
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; }
/// <summary>
/// The list of member properties
/// </summary>
/// <remarks>
/// Adding items to this list on the front-end will not add properties to the member in the database.
/// </remarks>
public List<MemberPropertyModel> MemberProperties { get; set; } = new List<MemberPropertyModel>();
}
/// <summary>
/// The list of member properties
/// </summary>
/// <remarks>
/// Adding items to this list on the front-end will not add properties to the member in the database.
/// </remarks>
public List<MemberPropertyModel> MemberProperties { get; set; } = new();
}

View File

@@ -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<ProfileModel?> BuildForCurrentMemberAsync()
{
IMemberManager? memberManager =
_httpContextAccessor.HttpContext?.RequestServices.GetRequiredService<IMemberManager>();
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<ProfileModel?> 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<IMemberManager>();
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;
}
}

View File

@@ -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<MemberPropertyModel>();
}
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; } = null!;
/// <summary>
/// Returns the member properties
/// </summary>
public List<MemberPropertyModel> MemberProperties { get; set; }
/// <summary>
/// The member type alias to use to register the member
/// </summary>
[Editable(false)]
public string MemberTypeAlias { get; set; }
/// <summary>
/// The members real name
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// The members password
/// </summary>
[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!;
/// <summary>
/// The username of the model, if UsernameIsEmail is true then this is ignored.
/// </summary>
public string? Username { get; set; }
/// <summary>
/// Flag to determine if the username should be the email address, if true then the Username property is ignored
/// </summary>
public bool UsernameIsEmail { get; set; }
MemberTypeAlias = Constants.Conventions.MemberTypes.DefaultAlias;
UsernameIsEmail = true;
MemberProperties = new List<MemberPropertyModel>();
}
[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; } = null!;
/// <summary>
/// Returns the member properties
/// </summary>
public List<MemberPropertyModel> MemberProperties { get; set; }
/// <summary>
/// The member type alias to use to register the member
/// </summary>
[Editable(false)]
public string MemberTypeAlias { get; set; }
/// <summary>
/// The members real name
/// </summary>
public string Name { get; set; } = null!;
/// <summary>
/// The members password
/// </summary>
[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!;
/// <summary>
/// The username of the model, if UsernameIsEmail is true then this is ignored.
/// </summary>
public string? Username { get; set; }
/// <summary>
/// Flag to determine if the username should be the email address, if true then the Username property is ignored
/// </summary>
public bool UsernameIsEmail { get; set; }
}

View File

@@ -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;
/// <summary>
/// Builds a <see cref="RegisterModel" /> for use on the front-end
/// </summary>
public class RegisterModelBuilder : MemberModelBuilderBase
{
private bool _lookupProperties;
private string? _memberTypeAlias;
private string? _redirectUrl;
private bool _usernameIsEmail;
/// <summary>
/// Builds a <see cref="RegisterModel"/> for use on the front-end
/// </summary>
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<MemberPropertyModel>().ToList()
};
return model;
}
MemberTypeAlias = providedOrDefaultMemberTypeAlias,
UsernameIsEmail = _usernameIsEmail,
MemberProperties = _lookupProperties
? GetMemberPropertiesViewModel(memberType)
: Enumerable.Empty<MemberPropertyModel>().ToList(),
};
return model;
}
}

View File

@@ -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;
/// <summary>
/// Used to find a controller/action in the current available routes
/// </summary>
public class ControllerActionSearcher : IControllerActionSearcher
{
private const string DefaultActionName = nameof(RenderController.Index);
private readonly IActionSelector _actionSelector;
private readonly ILogger<ControllerActionSearcher> _logger;
/// <summary>
/// Used to find a controller/action in the current available routes
/// Initializes a new instance of the <see cref="ControllerActionSearcher" /> class.
/// </summary>
public class ControllerActionSearcher : IControllerActionSearcher
public ControllerActionSearcher(
ILogger<ControllerActionSearcher> logger,
IActionSelector actionSelector)
{
private readonly ILogger<ControllerActionSearcher> _logger;
private readonly IActionSelector _actionSelector;
private const string DefaultActionName = nameof(RenderController.Index);
_logger = logger;
_actionSelector = actionSelector;
}
/// <summary>
/// Initializes a new instance of the <see cref="ControllerActionSearcher"/> class.
/// </summary>
public ControllerActionSearcher(
ILogger<ControllerActionSearcher> logger,
IActionSelector actionSelector)
/// <summary>
/// Determines if a custom controller can hijack the current route
/// </summary>
/// <typeparam name="T">The controller type to find</typeparam>
public ControllerActionDescriptor? Find<T>(HttpContext httpContext, string? controller, string? action) =>
Find<T>(httpContext, controller, action, null);
/// <summary>
/// Determines if a custom controller can hijack the current route
/// </summary>
/// <typeparam name="T">The controller type to find</typeparam>
public ControllerActionDescriptor? Find<T>(HttpContext httpContext, string? controller, string? action, string? area)
{
IReadOnlyList<ControllerActionDescriptor>? candidates =
FindControllerCandidates<T>(httpContext, controller, action, DefaultActionName, area);
if (candidates?.Count > 0)
{
_logger = logger;
_actionSelector = actionSelector;
return candidates[0];
}
return null;
}
/// <summary>
/// Determines if a custom controller can hijack the current route
/// </summary>
/// <typeparam name="T">The controller type to find</typeparam>
public ControllerActionDescriptor? Find<T>(HttpContext httpContext, string? controller, string? action) => Find<T>(httpContext, controller, action, null);
/// <summary>
/// Determines if a custom controller can hijack the current route
/// </summary>
/// <typeparam name="T">The controller type to find</typeparam>
public ControllerActionDescriptor? Find<T>(HttpContext httpContext, string? controller, string? action, string? area)
/// <summary>
/// Return a list of controller candidates that match the custom controller and action names
/// </summary>
private IReadOnlyList<ControllerActionDescriptor>? FindControllerCandidates<T>(
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<ControllerActionDescriptor>? candidates = FindControllerCandidates<T>(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 };
/// <summary>
/// Return a list of controller candidates that match the custom controller and action names
/// </summary>
private IReadOnlyList<ControllerActionDescriptor>? FindControllerCandidates<T>(
HttpContext httpContext,
string? customControllerName,
string? customActionName,
string? defaultActionName,
string? area = null)
// try finding candidates for the custom action
var candidates = _actionSelector.SelectCandidates(routeContext)?
.Cast<ControllerActionDescriptor>()
.Where(x => TypeHelper.IsTypeAssignableFrom<T>(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<ControllerActionDescriptor>()
.Where(x => TypeHelper.IsTypeAssignableFrom<T>(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<ControllerActionDescriptor>()
.Where(x => TypeHelper.IsTypeAssignableFrom<T>(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<ControllerActionDescriptor>()
.Where(x => TypeHelper.IsTypeAssignableFrom<T>(x.ControllerTypeInfo))
.ToList();
return candidates;
}
}

View File

@@ -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;
/// <summary>
/// Creates routes for surface controllers
/// </summary>
public sealed class FrontEndRoutes : IAreaRoutes
{
private readonly UmbracoApiControllerTypeCollection _apiControllers;
private readonly IRuntimeState _runtimeState;
private readonly SurfaceControllerTypeCollection _surfaceControllerTypeCollection;
private readonly string _umbracoPathSegment;
/// <summary>
/// Creates routes for surface controllers
/// Initializes a new instance of the <see cref="FrontEndRoutes" /> class.
/// </summary>
public sealed class FrontEndRoutes : IAreaRoutes
public FrontEndRoutes(
IOptions<GlobalSettings> 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);
}
/// <summary>
/// Initializes a new instance of the <see cref="FrontEndRoutes"/> class.
/// </summary>
public FrontEndRoutes(
IOptions<GlobalSettings> globalSettings,
IHostingEnvironment hostingEnvironment,
IRuntimeState runtimeState,
SurfaceControllerTypeCollection surfaceControllerTypeCollection,
UmbracoApiControllerTypeCollection apiControllers)
/// <inheritdoc />
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;
}
/// <inheritdoc/>
public void CreateRoutes(IEndpointRouteBuilder endpoints)
{
if (_runtimeState.Level != RuntimeLevel.Run)
{
return;
}
AutoRouteSurfaceControllers(endpoints);
AutoRouteFrontEndApiControllers(endpoints);
}
AutoRouteSurfaceControllers(endpoints);
AutoRouteFrontEndApiControllers(endpoints);
/// <summary>
/// Auto-routes all front-end surface controllers
/// </summary>
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);
}
}
/// <summary>
/// Auto-routes all front-end surface controllers
/// </summary>
private void AutoRouteSurfaceControllers(IEndpointRouteBuilder endpoints)
/// <summary>
/// Auto-routes all front-end api controllers
/// </summary>
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;
}
}
/// <summary>
/// Auto-routes all front-end api controllers
/// </summary>
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)
}
}
}

View File

@@ -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<T>(HttpContext httpContext, string? controller, string? action);
ControllerActionDescriptor? Find<T>(HttpContext httpContext, string? controller, string? action);
ControllerActionDescriptor? Find<T>(HttpContext httpContext, string? controller, string? action, string? area)
=> Find<T>(httpContext, controller, action);
}
ControllerActionDescriptor? Find<T>(HttpContext httpContext, string? controller, string? action, string? area)
=> Find<T>(httpContext, controller, action);
}

View File

@@ -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
{
/// <summary>
/// Ensures that access to current node is permitted.
/// </summary>
/// <param name="httpContext"></param>
/// <param name="routeValues">The current route values</param>
/// <returns>Updated route values if public access changes the rendered content, else the original route values.</returns>
/// <remarks>Redirecting to a different site root and/or culture will not pick the new site root nor the new culture.</remarks>
Task<UmbracoRouteValues?> RewriteForPublishedContentAccessAsync(HttpContext httpContext, UmbracoRouteValues routeValues);
}
/// <summary>
/// Ensures that access to current node is permitted.
/// </summary>
/// <param name="httpContext"></param>
/// <param name="routeValues">The current route values</param>
/// <returns>Updated route values if public access changes the rendered content, else the original route values.</returns>
/// <remarks>Redirecting to a different site root and/or culture will not pick the new site root nor the new culture.</remarks>
Task<UmbracoRouteValues?> RewriteForPublishedContentAccessAsync(
HttpContext httpContext,
UmbracoRouteValues routeValues);
}

View File

@@ -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;
/// <summary>
/// Used to create <see cref="UmbracoRouteValues" />
/// </summary>
public interface IUmbracoRouteValuesFactory
{
/// <summary>
/// Used to create <see cref="UmbracoRouteValues"/>
/// Creates <see cref="UmbracoRouteValues" />
/// </summary>
public interface IUmbracoRouteValuesFactory
{
/// <summary>
/// Creates <see cref="UmbracoRouteValues"/>
/// </summary>
Task<UmbracoRouteValues> CreateAsync(HttpContext httpContext, IPublishedRequest request);
}
Task<UmbracoRouteValues> CreateAsync(HttpContext httpContext, IPublishedRequest request);
}

View File

@@ -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;
/// <summary>
/// Used to handle 404 routes that haven't been handled by the end user
/// </summary>
internal class NotFoundSelectorPolicy : MatcherPolicy, IEndpointSelectorPolicy
{
/// <summary>
/// Used to handle 404 routes that haven't been handled by the end user
/// </summary>
internal class NotFoundSelectorPolicy : MatcherPolicy, IEndpointSelectorPolicy
private readonly EndpointDataSource _endpointDataSource;
private readonly Lazy<Endpoint> _notFound;
public NotFoundSelectorPolicy(EndpointDataSource endpointDataSource)
{
private readonly Lazy<Endpoint> _notFound;
private readonly EndpointDataSource _endpointDataSource;
_notFound = new Lazy<Endpoint>(GetNotFoundEndpoint);
_endpointDataSource = endpointDataSource;
}
public NotFoundSelectorPolicy(EndpointDataSource endpointDataSource)
{
_notFound = new Lazy<Endpoint>(GetNotFoundEndpoint);
_endpointDataSource = endpointDataSource;
}
public override int Order => 0;
// return the endpoint for the RenderController.Index action.
private Endpoint GetNotFoundEndpoint()
public bool AppliesToEndpoints(IReadOnlyList<Endpoint> 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<ControllerAttribute>();
if (controller != null)
{
// return the endpoint for the RenderController.Index action.
ControllerActionDescriptor? descriptor = x.Metadata?.GetMetadata<ControllerActionDescriptor>();
return descriptor?.ControllerTypeInfo == typeof(RenderController)
&& descriptor?.ActionName == nameof(RenderController.Index);
});
return e;
}
public override int Order => 0;
public bool AppliesToEndpoints(IReadOnlyList<Endpoint> 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<ControllerAttribute>();
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<IDynamicEndpointMetadata>() != 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<IDynamicEndpointMetadata>() != null);
}
public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
{
if (AllInvalid(candidates))
{
if (AllInvalid(candidates))
UmbracoRouteValues? umbracoRouteValues = httpContext.Features.Get<UmbracoRouteValues>();
if (umbracoRouteValues?.PublishedRequest != null
&& !umbracoRouteValues.PublishedRequest.HasPublishedContent()
&& umbracoRouteValues.PublishedRequest.ResponseStatusCode == StatusCodes.Status404NotFound)
{
UmbracoRouteValues? umbracoRouteValues = httpContext.Features.Get<UmbracoRouteValues>();
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<IgnoreFromNotFoundSelectorPolicyAttribute>() is null)
{
return false;
}
}
// return the endpoint for the RenderController.Index action.
ControllerActionDescriptor? descriptor = x.Metadata.GetMetadata<ControllerActionDescriptor>();
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<IgnoreFromNotFoundSelectorPolicyAttribute>() is null)
{
return false;
}
}
return true;
}
}

View File

@@ -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<PublicAccessRequestHandler> _logger;
private readonly IPublicAccessChecker _publicAccessChecker;
private readonly IPublicAccessService _publicAccessService;
private readonly IPublishedRouter _publishedRouter;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IUmbracoRouteValuesFactory _umbracoRouteValuesFactory;
public PublicAccessRequestHandler(
ILogger<PublicAccessRequestHandler> logger,
IPublicAccessService publicAccessService,
IPublicAccessChecker publicAccessChecker,
IUmbracoContextAccessor umbracoContextAccessor,
IUmbracoRouteValuesFactory umbracoRouteValuesFactory,
IPublishedRouter publishedRouter)
{
private readonly ILogger<PublicAccessRequestHandler> _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<PublicAccessRequestHandler> logger,
IPublicAccessService publicAccessService,
IPublicAccessChecker publicAccessChecker,
IUmbracoContextAccessor umbracoContextAccessor,
IUmbracoRouteValuesFactory umbracoRouteValuesFactory,
IPublishedRouter publishedRouter)
/// <inheritdoc />
public async Task<UmbracoRouteValues?> 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);
/// <inheritdoc />
public async Task<UmbracoRouteValues?> 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<PublicAccessEntry?> 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<PublicAccessEntry?> publicAccessAttempt = _publicAccessService.IsProtected(path);
private async Task<UmbracoRouteValues> 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<UmbracoRouteValues> 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.");
}
}

View File

@@ -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;
/// <summary>
/// The route value transformer for Umbraco front-end routes
/// </summary>
/// <remarks>
/// 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
/// </remarks>
public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer
{
private readonly IControllerActionSearcher _controllerActionSearcher;
private readonly IDataProtectionProvider _dataProtectionProvider;
private readonly ILogger<UmbracoRouteValueTransformer> _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<GlobalSettings>, IHostingEnvironment & IEventAggregator instead")]
public UmbracoRouteValueTransformer(
ILogger<UmbracoRouteValueTransformer> logger,
IUmbracoContextAccessor umbracoContextAccessor,
IPublishedRouter publishedRouter,
IOptions<GlobalSettings> 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)
{
}
/// <summary>
/// The route value transformer for Umbraco front-end routes
/// Initializes a new instance of the <see cref="UmbracoRouteValueTransformer" /> class.
/// </summary>
/// <remarks>
/// 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
/// </remarks>
public class UmbracoRouteValueTransformer : DynamicRouteValueTransformer
public UmbracoRouteValueTransformer(
ILogger<UmbracoRouteValueTransformer> logger,
IUmbracoContextAccessor umbracoContextAccessor,
IPublishedRouter publishedRouter,
IRuntimeState runtime,
IUmbracoRouteValuesFactory routeValuesFactory,
IRoutableDocumentFilter routableDocumentFilter,
IDataProtectionProvider dataProtectionProvider,
IControllerActionSearcher controllerActionSearcher,
IPublicAccessRequestHandler publicAccessRequestHandler)
{
private readonly ILogger<UmbracoRouteValueTransformer> _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;
}
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoRouteValueTransformer"/> class.
/// </summary>
public UmbracoRouteValueTransformer(
ILogger<UmbracoRouteValueTransformer> logger,
IUmbracoContextAccessor umbracoContextAccessor,
IPublishedRouter publishedRouter,
IOptions<GlobalSettings> globalSettings,
IHostingEnvironment hostingEnvironment,
IRuntimeState runtime,
IUmbracoRouteValuesFactory routeValuesFactory,
IRoutableDocumentFilter routableDocumentFilter,
IDataProtectionProvider dataProtectionProvider,
IControllerActionSearcher controllerActionSearcher,
IEventAggregator eventAggregator,
IPublicAccessRequestHandler publicAccessRequestHandler)
/// <inheritdoc />
public override async ValueTask<RouteValueDictionary> TransformAsync(
HttpContext httpContext, RouteValueDictionary values)
{
// If we aren't running, then we have nothing to route
if (_runtime.Level != RuntimeLevel.Run)
{
if (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!;
}
/// <inheritdoc/>
public override async ValueTask<RouteValueDictionary> 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<UmbracoRouteValues>();
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<UmbracoRouteValues>();
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<RenderNoContentController>(),
[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<RenderNoContentController>(),
[ActionToken] = nameof(RenderNoContentController.Index),
};
if (string.IsNullOrWhiteSpace(umbracoRouteValues?.ActionName) == false)
{
newValues[ActionToken] = umbracoRouteValues.ActionName;
}
return newValues;
}
private async Task<IPublishedRequest> 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);
}
/// <summary>
/// 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.
/// </summary>
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<IPublishedRequest> 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;
}
/// <summary>
/// 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.
/// </summary>
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<string, string?>? 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<string, string?> 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<SurfaceController>(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<string, string?> 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<SurfaceController>(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 };
}
}

View File

@@ -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;
/// <summary>
/// Used to create <see cref="UmbracoRouteValues" />
/// </summary>
public class UmbracoRouteValuesFactory : IUmbracoRouteValuesFactory
{
private readonly IControllerActionSearcher _controllerActionSearcher;
private readonly Lazy<ControllerActionDescriptor> _defaultControllerDescriptor;
private readonly Lazy<string> _defaultControllerName;
private readonly IPublishedRouter _publishedRouter;
private readonly IShortStringHelper _shortStringHelper;
private readonly UmbracoFeatures _umbracoFeatures;
/// <summary>
/// Used to create <see cref="UmbracoRouteValues"/>
/// Initializes a new instance of the <see cref="UmbracoRouteValuesFactory" /> class.
/// </summary>
public class UmbracoRouteValuesFactory : IUmbracoRouteValuesFactory
public UmbracoRouteValuesFactory(
IOptions<UmbracoRenderingDefaultsOptions> 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<string> _defaultControllerName;
private readonly Lazy<ControllerActionDescriptor> _defaultControllerDescriptor;
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoRouteValuesFactory"/> class.
/// </summary>
public UmbracoRouteValuesFactory(
IOptions<UmbracoRenderingDefaultsOptions> renderingDefaults,
IShortStringHelper shortStringHelper,
UmbracoFeatures umbracoFeatures,
IControllerActionSearcher controllerActionSearcher,
IPublishedRouter publishedRouter)
_shortStringHelper = shortStringHelper;
_umbracoFeatures = umbracoFeatures;
_controllerActionSearcher = controllerActionSearcher;
_publishedRouter = publishedRouter;
_defaultControllerName = new Lazy<string>(() =>
ControllerExtensions.GetControllerName(renderingDefaults.Value.DefaultControllerType));
_defaultControllerDescriptor = new Lazy<ControllerActionDescriptor>(() =>
{
_shortStringHelper = shortStringHelper;
_umbracoFeatures = umbracoFeatures;
_controllerActionSearcher = controllerActionSearcher;
_publishedRouter = publishedRouter;
_defaultControllerName = new Lazy<string>(() => ControllerExtensions.GetControllerName(renderingDefaults.Value.DefaultControllerType));
_defaultControllerDescriptor = new Lazy<ControllerActionDescriptor>(() =>
ControllerActionDescriptor? descriptor = _controllerActionSearcher.Find<IRenderController>(
new DefaultHttpContext(), // this actually makes no difference for this method
DefaultControllerName,
UmbracoRouteValues.DefaultActionName);
if (descriptor == null)
{
ControllerActionDescriptor? descriptor = _controllerActionSearcher.Find<IRenderController>(
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;
});
/// <summary>
/// Gets the default controller name
/// </summary>
protected string DefaultControllerName => _defaultControllerName.Value;
/// <inheritdoc />
public async Task<UmbracoRouteValues> CreateAsync(HttpContext httpContext, IPublishedRequest request)
{
if (httpContext is null)
{
throw new ArgumentNullException(nameof(httpContext));
}
/// <summary>
/// Gets the default controller name
/// </summary>
protected string DefaultControllerName => _defaultControllerName.Value;
/// <inheritdoc/>
public async Task<UmbracoRouteValues> 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;
}
/// <summary>
/// Check if the route is hijacked and return new route values
/// </summary>
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<IRenderController>(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;
}
/// <summary>
/// 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
/// </summary>
private async Task<UmbracoRouteValues> 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;
}
/// <summary>
/// Check if the route is hijacked and return new route values
/// </summary>
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<IRenderController>(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;
}
/// <summary>
/// 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
/// </summary>
private async Task<UmbracoRouteValues> 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;
}
}

View File

@@ -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;
/// <summary>
/// Custom <see cref="AuthenticationBuilder" /> used to associate external logins with umbraco external login options
/// </summary>
public class MemberAuthenticationBuilder : AuthenticationBuilder
{
private readonly Action<MemberExternalLoginProviderOptions> _loginProviderOptions;
public MemberAuthenticationBuilder(
IServiceCollection services,
Action<MemberExternalLoginProviderOptions>? loginProviderOptions = null)
: base(services)
=> _loginProviderOptions = loginProviderOptions ?? (x => { });
public string SchemeForMembers(string scheme)
=> scheme.EnsureStartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix);
/// <summary>
/// Custom <see cref="AuthenticationBuilder"/> used to associate external logins with umbraco external login options
/// Overridden to track the final authenticationScheme being registered for the external login
/// </summary>
public class MemberAuthenticationBuilder : AuthenticationBuilder
/// <typeparam name="TOptions"></typeparam>
/// <typeparam name="THandler"></typeparam>
/// <param name="authenticationScheme"></param>
/// <param name="displayName"></param>
/// <param name="configureOptions"></param>
/// <returns></returns>
public override AuthenticationBuilder AddRemoteScheme<TOptions, THandler>(
string authenticationScheme, string? displayName, Action<TOptions>? configureOptions)
{
private readonly Action<MemberExternalLoginProviderOptions> _loginProviderOptions;
public MemberAuthenticationBuilder(
IServiceCollection services,
Action<MemberExternalLoginProviderOptions>? loginProviderOptions = null)
: base(services)
=> _loginProviderOptions = loginProviderOptions ?? (x => { });
public string? SchemeForMembers(string scheme)
=> scheme?.EnsureStartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix);
/// <summary>
/// Overridden to track the final authenticationScheme being registered for the external login
/// </summary>
/// <typeparam name="TOptions"></typeparam>
/// <typeparam name="THandler"></typeparam>
/// <param name="authenticationScheme"></param>
/// <param name="displayName"></param>
/// <param name="configureOptions"></param>
/// <returns></returns>
public override AuthenticationBuilder AddRemoteScheme<TOptions, THandler>(string authenticationScheme, string? displayName, Action<TOptions>? 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<IOptionsMonitor<MemberExternalLoginProviderOptions>>());
});
Services.TryAddEnumerable(ServiceDescriptor.Singleton<IPostConfigureOptions<TOptions>, EnsureMemberScheme<TOptions>>());
return base.AddRemoteScheme<TOptions, THandler>(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<TOptions> : IPostConfigureOptions<TOptions> 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<IOptionsMonitor<MemberExternalLoginProviderOptions>>());
});
Services.TryAddEnumerable(ServiceDescriptor
.Singleton<IPostConfigureOptions<TOptions>, EnsureMemberScheme<TOptions>>());
options.SignInScheme = IdentityConstants.ExternalScheme;
}
}
return base.AddRemoteScheme<TOptions, THandler>(authenticationScheme, displayName, configureOptions);
}
// Ensures that the sign in scheme is always the Umbraco member external type
private class EnsureMemberScheme<TOptions> : IPostConfigureOptions<TOptions>
where TOptions : RemoteAuthenticationOptions
{
public void PostConfigure(string name, TOptions options)
{
if (!name.StartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix))
{
return;
}
options.SignInScheme = IdentityConstants.ExternalScheme;
}
}
}

View File

@@ -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;
/// <summary>
/// Used to add back office login providers
/// </summary>
public class MemberExternalLoginsBuilder
{
private readonly IServiceCollection _services;
public MemberExternalLoginsBuilder(IServiceCollection services) => _services = services;
/// <summary>
/// Used to add back office login providers
/// Add a back office login provider with options
/// </summary>
public class MemberExternalLoginsBuilder
/// <param name="loginProviderOptions"></param>
/// <param name="build"></param>
/// <returns></returns>
public MemberExternalLoginsBuilder AddMemberLogin(
Action<MemberAuthenticationBuilder> build,
Action<MemberExternalLoginProviderOptions>? loginProviderOptions = null)
{
public MemberExternalLoginsBuilder(IServiceCollection services)
{
_services = services;
}
private readonly IServiceCollection _services;
/// <summary>
/// Add a back office login provider with options
/// </summary>
/// <param name="loginProviderOptions"></param>
/// <param name="build"></param>
/// <returns></returns>
public MemberExternalLoginsBuilder AddMemberLogin(
Action<MemberAuthenticationBuilder> build,
Action<MemberExternalLoginProviderOptions>? loginProviderOptions = null)
{
build(new MemberAuthenticationBuilder(_services, loginProviderOptions));
return this;
}
build(new MemberAuthenticationBuilder(_services, loginProviderOptions));
return this;
}
}

View File

@@ -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
{
/// <summary>
/// Configure view engine locations for front-end rendering based on App_Plugins views
/// </summary>
public class PluginRazorViewEngineOptionsSetup : IConfigureOptions<RazorViewEngineOptions>
{
/// <inheritdoc/>
public void Configure(RazorViewEngineOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
namespace Umbraco.Cms.Web.Website.ViewEngines;
options.ViewLocationExpanders.Add(new ViewLocationExpander());
/// <summary>
/// Configure view engine locations for front-end rendering based on App_Plugins views
/// </summary>
public class PluginRazorViewEngineOptionsSetup : IConfigureOptions<RazorViewEngineOptions>
{
/// <inheritdoc />
public void Configure(RazorViewEngineOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
/// <summary>
/// Expands the default view locations
/// </summary>
private class ViewLocationExpander : IViewLocationExpander
options.ViewLocationExpanders.Add(new ViewLocationExpander());
}
/// <summary>
/// Expands the default view locations
/// </summary>
private class ViewLocationExpander : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(
ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> 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)
{
}
}
}

View File

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

View File

@@ -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;
/// <summary>
/// Wraps all view engines with a <see cref="ProfilingViewEngine" />
/// </summary>
public class ProfilingViewEngineWrapperMvcViewOptionsSetup : IConfigureOptions<MvcViewOptions>
{
private readonly IProfiler _profiler;
/// <summary>
/// Wraps all view engines with a <see cref="ProfilingViewEngine"/>
/// Initializes a new instance of the <see cref="ProfilingViewEngineWrapperMvcViewOptionsSetup" /> class.
/// </summary>
public class ProfilingViewEngineWrapperMvcViewOptionsSetup : IConfigureOptions<MvcViewOptions>
/// <param name="profiler">The <see cref="IProfiler" /></param>
public ProfilingViewEngineWrapperMvcViewOptionsSetup(IProfiler profiler) =>
_profiler = profiler ?? throw new ArgumentNullException(nameof(profiler));
/// <inheritdoc />
public void Configure(MvcViewOptions options)
{
private readonly IProfiler _profiler;
/// <summary>
/// Initializes a new instance of the <see cref="ProfilingViewEngineWrapperMvcViewOptionsSetup"/> class.
/// </summary>
/// <param name="profiler">The <see cref="IProfiler"/></param>
public ProfilingViewEngineWrapperMvcViewOptionsSetup(IProfiler profiler) => _profiler = profiler ?? throw new ArgumentNullException(nameof(profiler));
/// <inheritdoc/>
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<IViewEngine> 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<IViewEngine> 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);
}
}
}

View File

@@ -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
{
/// <summary>
/// Configure view engine locations for front-end rendering
/// </summary>
public class RenderRazorViewEngineOptionsSetup : IConfigureOptions<RazorViewEngineOptions>
{
/// <inheritdoc/>
public void Configure(RazorViewEngineOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
namespace Umbraco.Cms.Web.Website.ViewEngines;
options.ViewLocationExpanders.Add(new ViewLocationExpander());
/// <summary>
/// Configure view engine locations for front-end rendering
/// </summary>
public class RenderRazorViewEngineOptionsSetup : IConfigureOptions<RazorViewEngineOptions>
{
/// <inheritdoc />
public void Configure(RazorViewEngineOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
/// <summary>
/// Expands the default view locations
/// </summary>
private class ViewLocationExpander : IViewLocationExpander
options.ViewLocationExpanders.Add(new ViewLocationExpander());
}
/// <summary>
/// Expands the default view locations
/// </summary>
private class ViewLocationExpander : IViewLocationExpander
{
public IEnumerable<string> ExpandViewLocations(
ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
{
public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> 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)
{
}
}
}

View File

@@ -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<IUmbracoContext>();
mockUmbracoContext.Setup(x => x.Content.HasContent()).Returns(true);
var mockIOHelper = new Mock<IIOHelper>();
var controller = new RenderNoContentController(new TestUmbracoContextAccessor(mockUmbracoContext.Object), mockIOHelper.Object, new TestOptionsSnapshot<GlobalSettings>(new GlobalSettings()));
var mockHostingEnvironment = new Mock<IHostingEnvironment>();
var controller = new RenderNoContentController(new TestUmbracoContextAccessor(mockUmbracoContext.Object), new TestOptionsSnapshot<GlobalSettings>(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<IIOHelper>();
mockIOHelper.Setup(x => x.ResolveUrl(It.Is<string>(y => y == UmbracoPathSetting))).Returns(UmbracoPath);
var mockHostingEnvironment = new Mock<IHostingEnvironment>();
mockHostingEnvironment.Setup(x => x.ToAbsolute(It.Is<string>(y => y == UmbracoPathSetting)))
.Returns(UmbracoPath);
var globalSettings = new TestOptionsSnapshot<GlobalSettings>(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);