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:
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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\"");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user