Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2890e15c3 | ||
|
|
a352c42742 | ||
|
|
9f37db18d1 | ||
|
|
59ad07209b | ||
|
|
67abecc252 | ||
|
|
ebd0017f6e | ||
|
|
417f15197e | ||
|
|
ce40103c4f | ||
|
|
2748fdfc48 | ||
|
|
53cc663bde | ||
|
|
13a2cd71c4 | ||
|
|
a60ccd389b | ||
|
|
fd95dc3915 | ||
|
|
990e379ea8 | ||
|
|
b4144564c8 | ||
|
|
f6dbe0f33e |
@@ -1,5 +1,6 @@
|
||||
using Microsoft.AspNetCore.OutputCaching;
|
||||
using Microsoft.AspNetCore.OutputCaching;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Caching;
|
||||
@@ -7,9 +8,13 @@ namespace Umbraco.Cms.Api.Delivery.Caching;
|
||||
internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
|
||||
{
|
||||
private readonly TimeSpan _duration;
|
||||
private readonly StringValues _varyByHeaderNames;
|
||||
|
||||
public DeliveryApiOutputCachePolicy(TimeSpan duration)
|
||||
=> _duration = duration;
|
||||
public DeliveryApiOutputCachePolicy(TimeSpan duration, StringValues varyByHeaderNames)
|
||||
{
|
||||
_duration = duration;
|
||||
_varyByHeaderNames = varyByHeaderNames;
|
||||
}
|
||||
|
||||
ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
@@ -18,8 +23,14 @@ internal sealed class DeliveryApiOutputCachePolicy : IOutputCachePolicy
|
||||
.RequestServices
|
||||
.GetRequiredService<IRequestPreviewService>();
|
||||
|
||||
context.EnableOutputCaching = requestPreviewService.IsPreview() is false;
|
||||
IApiAccessService apiAccessService = context
|
||||
.HttpContext
|
||||
.RequestServices
|
||||
.GetRequiredService<IApiAccessService>();
|
||||
|
||||
context.EnableOutputCaching = requestPreviewService.IsPreview() is false && apiAccessService.HasPublicAccess();
|
||||
context.ResponseExpirationTimeSpan = _duration;
|
||||
context.CacheVaryByRules.HeaderNames = _varyByHeaderNames;
|
||||
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
private readonly IRequestRedirectService _requestRedirectService;
|
||||
private readonly IRequestPreviewService _requestPreviewService;
|
||||
private readonly IRequestMemberAccessService _requestMemberAccessService;
|
||||
private const string PreviewContentRequestPathPrefix = $"/{Constants.DeliveryApi.Routing.PreviewContentPathPrefix}";
|
||||
private const string PreviewContentRequestPathPrefix = $"/{Umbraco.Cms.Core.Constants.DeliveryApi.Routing.PreviewContentPathPrefix}";
|
||||
|
||||
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
|
||||
public ByRouteContentApiController(
|
||||
|
||||
@@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Delivery.Accessors;
|
||||
using Umbraco.Cms.Api.Delivery.Caching;
|
||||
@@ -108,12 +109,20 @@ public static class UmbracoBuilderExtensions
|
||||
|
||||
if (outputCacheSettings.ContentDuration.TotalSeconds > 0)
|
||||
{
|
||||
options.AddPolicy(Constants.DeliveryApi.OutputCache.ContentCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.ContentDuration));
|
||||
options.AddPolicy(
|
||||
Constants.DeliveryApi.OutputCache.ContentCachePolicy,
|
||||
new DeliveryApiOutputCachePolicy(
|
||||
outputCacheSettings.ContentDuration,
|
||||
new StringValues([Constants.DeliveryApi.HeaderNames.AcceptLanguage, Constants.DeliveryApi.HeaderNames.StartItem])));
|
||||
}
|
||||
|
||||
if (outputCacheSettings.MediaDuration.TotalSeconds > 0)
|
||||
{
|
||||
options.AddPolicy(Constants.DeliveryApi.OutputCache.MediaCachePolicy, new DeliveryApiOutputCachePolicy(outputCacheSettings.MediaDuration));
|
||||
options.AddPolicy(
|
||||
Constants.DeliveryApi.OutputCache.MediaCachePolicy,
|
||||
new DeliveryApiOutputCachePolicy(
|
||||
outputCacheSettings.MediaDuration,
|
||||
Constants.DeliveryApi.HeaderNames.StartItem));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
@@ -21,7 +21,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Accept-Language",
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.AcceptLanguage,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Defines the language to return. Use this when querying language variant content items.",
|
||||
@@ -37,7 +37,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Preview",
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.Preview,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "Whether to request draft content.",
|
||||
@@ -46,7 +46,7 @@ internal sealed class SwaggerContentDocumentationFilter : SwaggerDocumentationFi
|
||||
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Start-Item",
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.StartItem,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "URL segment or GUID of a root content item.",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
@@ -63,7 +63,7 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController>
|
||||
protected void AddApiKey(OpenApiOperation operation) =>
|
||||
operation.Parameters.Add(new OpenApiParameter
|
||||
{
|
||||
Name = "Api-Key",
|
||||
Name = Core.Constants.DeliveryApi.HeaderNames.ApiKey,
|
||||
In = ParameterLocation.Header,
|
||||
Required = false,
|
||||
Description = "API key specified through configuration to authorize access to the API.",
|
||||
|
||||
+48
-21
@@ -6,6 +6,7 @@ using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
using Umbraco.Cms.Infrastructure.Security;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Handlers;
|
||||
@@ -16,16 +17,21 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
|
||||
private readonly ILogger<InitializeMemberApplicationNotificationHandler> _logger;
|
||||
private readonly DeliveryApiSettings _deliveryApiSettings;
|
||||
private readonly IServiceScopeFactory _serviceScopeFactory;
|
||||
private readonly IServerRoleAccessor _serverRoleAccessor;
|
||||
private static readonly SemaphoreSlim _locker = new(1);
|
||||
private static bool _isInitialized = false;
|
||||
|
||||
public InitializeMemberApplicationNotificationHandler(
|
||||
IRuntimeState runtimeState,
|
||||
IOptions<DeliveryApiSettings> deliveryApiSettings,
|
||||
ILogger<InitializeMemberApplicationNotificationHandler> logger,
|
||||
IServiceScopeFactory serviceScopeFactory)
|
||||
IServiceScopeFactory serviceScopeFactory,
|
||||
IServerRoleAccessor serverRoleAccessor)
|
||||
{
|
||||
_runtimeState = runtimeState;
|
||||
_logger = logger;
|
||||
_serviceScopeFactory = serviceScopeFactory;
|
||||
_serverRoleAccessor = serverRoleAccessor;
|
||||
_deliveryApiSettings = deliveryApiSettings.Value;
|
||||
}
|
||||
|
||||
@@ -36,34 +42,55 @@ internal sealed class InitializeMemberApplicationNotificationHandler : INotifica
|
||||
return;
|
||||
}
|
||||
|
||||
// we cannot inject the IMemberApplicationManager because it ultimately takes a dependency on the DbContext ... and during
|
||||
// install that is not allowed (no connection string means no DbContext)
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMemberApplicationManager memberApplicationManager = scope.ServiceProvider.GetRequiredService<IMemberApplicationManager>();
|
||||
|
||||
if (_deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is not true)
|
||||
if (_serverRoleAccessor.CurrentServerRole is ServerRole.Subscriber)
|
||||
{
|
||||
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
|
||||
// subscriber instances should not alter the member application
|
||||
return;
|
||||
}
|
||||
|
||||
if (ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls) is false)
|
||||
try
|
||||
{
|
||||
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
await _locker.WaitAsync(cancellationToken);
|
||||
if (_isInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls.Any()
|
||||
&& ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls) is false)
|
||||
_isInitialized = true;
|
||||
|
||||
// we cannot inject the IMemberApplicationManager because it ultimately takes a dependency on the DbContext ... and during
|
||||
// install that is not allowed (no connection string means no DbContext)
|
||||
using IServiceScope scope = _serviceScopeFactory.CreateScope();
|
||||
IMemberApplicationManager memberApplicationManager = scope.ServiceProvider.GetRequiredService<IMemberApplicationManager>();
|
||||
|
||||
if (_deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is not true)
|
||||
{
|
||||
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls) is false)
|
||||
{
|
||||
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls.Any()
|
||||
&& ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls) is false)
|
||||
{
|
||||
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await memberApplicationManager.EnsureMemberApplicationAsync(
|
||||
_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls,
|
||||
_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls,
|
||||
cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
|
||||
return;
|
||||
_locker.Release();
|
||||
}
|
||||
|
||||
await memberApplicationManager.EnsureMemberApplicationAsync(
|
||||
_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls,
|
||||
_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private bool ValidateRedirectUrls(Uri[] redirectUrls)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
@@ -29,7 +29,7 @@ internal sealed class ApiAccessService : RequestHeaderHandler, IApiAccessService
|
||||
private bool IfEnabled(Func<bool> condition) => _deliveryApiSettings.Enabled && condition();
|
||||
|
||||
private bool HasValidApiKey() => _deliveryApiSettings.ApiKey.IsNullOrWhiteSpace() == false
|
||||
&& _deliveryApiSettings.ApiKey.Equals(GetHeaderValue("Api-Key"));
|
||||
&& _deliveryApiSettings.ApiKey.Equals(GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.ApiKey));
|
||||
|
||||
private bool IfMediaEnabled(Func<bool> condition) => _deliveryApiSettings is { Enabled: true, Media.Enabled: true } && condition();
|
||||
}
|
||||
|
||||
@@ -11,5 +11,5 @@ internal sealed class RequestPreviewService : RequestHeaderHandler, IRequestPrev
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsPreview() => string.Equals(GetHeaderValue("Preview"), "true", StringComparison.OrdinalIgnoreCase);
|
||||
public bool IsPreview() => string.Equals(GetHeaderValue(Core.Constants.DeliveryApi.HeaderNames.Preview), "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
@@ -58,5 +58,5 @@ internal sealed class RequestStartItemProvider : RequestHeaderHandler, IRequestS
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string? RequestedStartItem() => GetHeaderValue("Start-Item");
|
||||
public string? RequestedStartItem() => GetHeaderValue(Constants.DeliveryApi.HeaderNames.StartItem);
|
||||
}
|
||||
|
||||
@@ -159,15 +159,30 @@ public static class DistributedCacheExtensions
|
||||
=> dc.RefreshMemberCache(members.AsEnumerable());
|
||||
|
||||
public static void RefreshMemberCache(this DistributedCache dc, IEnumerable<IMember> members)
|
||||
=> dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.DistinctBy(x => (x.Id, x.Username)).Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, false)));
|
||||
|
||||
=> dc.RefreshByPayload(
|
||||
MemberCacheRefresher.UniqueId,
|
||||
GetPayloads(members, false));
|
||||
|
||||
[Obsolete("Use the overload accepting IEnumerable instead to avoid allocating arrays. This overload will be removed in Umbraco 13.")]
|
||||
public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members)
|
||||
=> dc.RemoveMemberCache(members.AsEnumerable());
|
||||
|
||||
public static void RemoveMemberCache(this DistributedCache dc, IEnumerable<IMember> members)
|
||||
=> dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.DistinctBy(x => (x.Id, x.Username)).Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, true)));
|
||||
=> dc.RefreshByPayload(
|
||||
MemberCacheRefresher.UniqueId,
|
||||
GetPayloads(members, true));
|
||||
|
||||
// Internal for unit test.
|
||||
internal static IEnumerable<MemberCacheRefresher.JsonPayload> GetPayloads(IEnumerable<IMember> members, bool removed)
|
||||
=> members
|
||||
.DistinctBy(x => (x.Id, x.Username))
|
||||
.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username, removed)
|
||||
{
|
||||
PreviousUsername = x.HasAdditionalData &&
|
||||
x.AdditionalData!.TryGetValue(Cms.Core.Constants.Entities.AdditionalDataKeys.MemberPreviousUserName, out var previousUsername)
|
||||
? previousUsername?.ToString()
|
||||
: null,
|
||||
});
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
namespace Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
|
||||
|
||||
internal class NoopMemberPartialViewCacheInvalidator : IMemberPartialViewCacheInvalidator
|
||||
{
|
||||
public void ClearPartialViewCacheItems(IEnumerable<int> memberIds)
|
||||
{
|
||||
// No operation performed, this is a no-op implementation.
|
||||
}
|
||||
}
|
||||
@@ -70,6 +70,8 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
|
||||
|
||||
public string? Username { get; }
|
||||
|
||||
public string? PreviousUsername { get; set; }
|
||||
|
||||
public bool Removed { get; }
|
||||
}
|
||||
|
||||
@@ -121,6 +123,13 @@ public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCache
|
||||
// https://github.com/umbraco/Umbraco-CMS/pull/17350
|
||||
// https://github.com/umbraco/Umbraco-CMS/pull/17815
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(CacheKeys.MemberUserNameCachePrefix + p.Username));
|
||||
|
||||
// If provided, clear the cache by the previous user name too.
|
||||
if (string.IsNullOrEmpty(p.PreviousUsername) is false)
|
||||
{
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(p.PreviousUsername));
|
||||
memberCache.Result?.Clear(RepositoryCacheKeys.GetKey<IMember, string>(CacheKeys.MemberUserNameCachePrefix + p.PreviousUsername));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Umbraco.Cms.Core;
|
||||
namespace Umbraco.Cms.Core;
|
||||
|
||||
public static partial class Constants
|
||||
{
|
||||
@@ -24,14 +24,42 @@ public static partial class Constants
|
||||
public static class OutputCache
|
||||
{
|
||||
/// <summary>
|
||||
/// Output cache policy name for content
|
||||
/// Output cache policy name for content.
|
||||
/// </summary>
|
||||
public const string ContentCachePolicy = "DeliveryApiContent";
|
||||
|
||||
/// <summary>
|
||||
/// Output cache policy name for media
|
||||
/// Output cache policy name for media.
|
||||
/// </summary>
|
||||
public const string MediaCachePolicy = "DeliveryApiMedia";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constants for Delivery API header names.
|
||||
/// </summary>
|
||||
public static class HeaderNames
|
||||
{
|
||||
/// <summary>
|
||||
/// Header name for accept language.
|
||||
/// </summary>
|
||||
public const string AcceptLanguage = "Accept-Language";
|
||||
|
||||
/// <summary>
|
||||
/// Header name for API key.
|
||||
/// </summary>
|
||||
public const string ApiKey = "Api-Key";
|
||||
|
||||
/// <summary>
|
||||
/// Header name for preview.
|
||||
/// </summary>
|
||||
public const string Preview = "Preview";
|
||||
|
||||
/// <summary>
|
||||
/// Header name for start item.
|
||||
/// </summary>
|
||||
public const string StartItem = "Start-Item";
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Umbraco.Cms.Core;
|
||||
|
||||
public static partial class Constants
|
||||
{
|
||||
public static class Entities
|
||||
{
|
||||
public static class AdditionalDataKeys
|
||||
{
|
||||
public const string MemberPreviousUserName = "previousUsername";
|
||||
|
||||
public const string MemberGroupPreviousName = "previousName";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,14 @@ using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Cache.PartialViewCacheInvalidators;
|
||||
using Umbraco.Cms.Core.Composing;
|
||||
using Umbraco.Cms.Core.Configuration;
|
||||
using Umbraco.Cms.Core.Configuration.Grid;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Diagnostics;
|
||||
using Umbraco.Cms.Core.Dictionary;
|
||||
using Umbraco.Cms.Core.DynamicRoot;
|
||||
using Umbraco.Cms.Core.Editors;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Features;
|
||||
@@ -35,7 +37,6 @@ using Umbraco.Cms.Core.Runtime;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.DynamicRoot;
|
||||
using Umbraco.Cms.Core.Sync;
|
||||
using Umbraco.Cms.Core.Telemetry;
|
||||
using Umbraco.Cms.Core.Templates;
|
||||
@@ -341,6 +342,11 @@ namespace Umbraco.Cms.Core.DependencyInjection
|
||||
// Data type configuration cache
|
||||
Services.AddUnique<IDataTypeConfigurationCache, DataTypeConfigurationCache>();
|
||||
Services.AddNotificationHandler<DataTypeCacheRefresherNotification, DataTypeConfigurationCacheRefresher>();
|
||||
|
||||
// Partial view cache invalidators (no-op, shipped implementation is added in Umbraco.Web.Website, but we
|
||||
// need this to ensure we have a service registered for this interface even in headless setups).
|
||||
// See: https://github.com/umbraco/Umbraco-CMS/issues/19661
|
||||
Services.AddUnique<IMemberPartialViewCacheInvalidator, NoopMemberPartialViewCacheInvalidator>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,16 +21,17 @@ public sealed class PublicAccessHandler :
|
||||
|
||||
private void Handle(IEnumerable<IMemberGroup> affectedEntities)
|
||||
{
|
||||
var keyName = Constants.Entities.AdditionalDataKeys.MemberGroupPreviousName;
|
||||
foreach (IMemberGroup grp in affectedEntities)
|
||||
{
|
||||
// check if the name has changed
|
||||
if ((grp.AdditionalData?.ContainsKey("previousName") ?? false)
|
||||
&& grp.AdditionalData["previousName"] != null
|
||||
&& grp.AdditionalData["previousName"]?.ToString().IsNullOrWhiteSpace() == false
|
||||
&& grp.AdditionalData["previousName"]?.ToString() != grp.Name)
|
||||
if ((grp.AdditionalData?.ContainsKey(keyName) ?? false)
|
||||
&& grp.AdditionalData[keyName] != null
|
||||
&& grp.AdditionalData[keyName]?.ToString().IsNullOrWhiteSpace() == false
|
||||
&& grp.AdditionalData[keyName]?.ToString() != grp.Name)
|
||||
{
|
||||
_publicAccessService.RenameMemberGroupRoleRules(
|
||||
grp.AdditionalData["previousName"]?.ToString(),
|
||||
grp.AdditionalData[keyName]?.ToString(),
|
||||
grp.Name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ _additionalData ??= new Dictionary<string, object?>();
|
||||
// if the name has changed, add the value to the additional data,
|
||||
// this is required purely for event handlers to know the previous name of the group
|
||||
// so we can keep the public access up to date.
|
||||
AdditionalData["previousName"] = _name;
|
||||
AdditionalData[Constants.Entities.AdditionalDataKeys.MemberGroupPreviousName] = _name;
|
||||
}
|
||||
|
||||
SetPropertyValueAndDetectChanges(value, ref _name, nameof(Name));
|
||||
|
||||
@@ -770,16 +770,27 @@ namespace Umbraco.Cms.Core.Services
|
||||
throw new ArgumentException("Cannot save member with empty name.");
|
||||
}
|
||||
|
||||
var previousUsername = _memberRepository.Get(member.Id)?.Username;
|
||||
|
||||
scope.WriteLock(Constants.Locks.MemberTree);
|
||||
|
||||
_memberRepository.Save(member);
|
||||
|
||||
if (publishNotificationSaveOptions.HasFlag(PublishNotificationSaveOptions.Saved))
|
||||
{
|
||||
scope.Notifications.Publish(
|
||||
savingNotification is null
|
||||
// If the user name has changed, populate the previous user name in the additional data, so the cache refreshers
|
||||
// have it available to clear the cache by the old name as well as the new.
|
||||
if (string.IsNullOrWhiteSpace(previousUsername) is false &&
|
||||
string.Equals(previousUsername, member.Username, StringComparison.OrdinalIgnoreCase) is false)
|
||||
{
|
||||
member.AdditionalData![Constants.Entities.AdditionalDataKeys.MemberPreviousUserName] = previousUsername;
|
||||
}
|
||||
|
||||
MemberSavedNotification memberSavedNotification = savingNotification is null
|
||||
? new MemberSavedNotification(member, evtMsgs)
|
||||
: new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification));
|
||||
: new MemberSavedNotification(member, evtMsgs).WithStateFrom(savingNotification);
|
||||
|
||||
scope.Notifications.Publish(memberSavedNotification);
|
||||
}
|
||||
|
||||
Audit(AuditType.Save, 0, member.Id);
|
||||
|
||||
+4
-2
@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Querying;
|
||||
@@ -25,8 +26,9 @@ internal class ContentTypeRepository : ContentTypeRepositoryBase<IContentType>,
|
||||
ILogger<ContentTypeRepository> logger,
|
||||
IContentTypeCommonRepository commonRepository,
|
||||
ILanguageRepository languageRepository,
|
||||
IShortStringHelper shortStringHelper)
|
||||
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper)
|
||||
IShortStringHelper shortStringHelper,
|
||||
Lazy<IIdKeyMap> idKeyMap)
|
||||
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper, idKeyMap)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+54
-25
@@ -29,15 +29,22 @@ internal abstract class ContentTypeRepositoryBase<TEntity> : EntityRepositoryBas
|
||||
where TEntity : class, IContentTypeComposition
|
||||
{
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
private readonly Lazy<IIdKeyMap> _idKeyMap;
|
||||
|
||||
protected ContentTypeRepositoryBase(IScopeAccessor scopeAccessor, AppCaches cache,
|
||||
ILogger<ContentTypeRepositoryBase<TEntity>> logger, IContentTypeCommonRepository commonRepository,
|
||||
ILanguageRepository languageRepository, IShortStringHelper shortStringHelper)
|
||||
protected ContentTypeRepositoryBase(
|
||||
IScopeAccessor scopeAccessor,
|
||||
AppCaches cache,
|
||||
ILogger<ContentTypeRepositoryBase<TEntity>> logger,
|
||||
IContentTypeCommonRepository commonRepository,
|
||||
ILanguageRepository languageRepository,
|
||||
IShortStringHelper shortStringHelper,
|
||||
Lazy<IIdKeyMap> idKeyMap)
|
||||
: base(scopeAccessor, cache, logger)
|
||||
{
|
||||
_shortStringHelper = shortStringHelper;
|
||||
CommonRepository = commonRepository;
|
||||
LanguageRepository = languageRepository;
|
||||
_idKeyMap = idKeyMap;
|
||||
}
|
||||
|
||||
protected IContentTypeCommonRepository CommonRepository { get; }
|
||||
@@ -287,7 +294,7 @@ AND umbracoNode.nodeObjectType = @objectType",
|
||||
// If the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias
|
||||
if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default)
|
||||
{
|
||||
AssignDataTypeFromPropertyEditor(propertyType);
|
||||
AssignDataTypeIdFromProvidedKeyOrPropertyEditor(propertyType);
|
||||
}
|
||||
|
||||
PropertyTypeDto propertyTypeDto =
|
||||
@@ -590,7 +597,7 @@ AND umbracoNode.id <> @id",
|
||||
// if the Id of the DataType is not set, we resolve it from the db by its PropertyEditorAlias
|
||||
if (propertyType.DataTypeId == 0 || propertyType.DataTypeId == default)
|
||||
{
|
||||
AssignDataTypeFromPropertyEditor(propertyType);
|
||||
AssignDataTypeIdFromProvidedKeyOrPropertyEditor(propertyType);
|
||||
}
|
||||
|
||||
// validate the alias
|
||||
@@ -1434,37 +1441,59 @@ AND umbracoNode.id <> @id",
|
||||
protected abstract TEntity? PerformGet(Guid id);
|
||||
|
||||
/// <summary>
|
||||
/// Try to set the data type id based on its ControlId
|
||||
/// Try to set the data type Id based on the provided key or property editor alias.
|
||||
/// </summary>
|
||||
/// <param name="propertyType"></param>
|
||||
private void AssignDataTypeFromPropertyEditor(IPropertyType propertyType)
|
||||
private void AssignDataTypeIdFromProvidedKeyOrPropertyEditor(IPropertyType propertyType)
|
||||
{
|
||||
// we cannot try to assign a data type of it's empty
|
||||
if (propertyType.PropertyEditorAlias.IsNullOrWhiteSpace() == false)
|
||||
// If a key is provided, use that.
|
||||
if (propertyType.DataTypeKey != Guid.Empty)
|
||||
{
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.Select<DataTypeDto>(dt => dt.Select(x => x.NodeDto))
|
||||
.From<DataTypeDto>()
|
||||
.InnerJoin<NodeDto>().On<DataTypeDto, NodeDto>((dt, n) => dt.NodeId == n.NodeId)
|
||||
.Where(
|
||||
"propertyEditorAlias = @propertyEditorAlias",
|
||||
new { propertyEditorAlias = propertyType.PropertyEditorAlias })
|
||||
.OrderBy<DataTypeDto>(typeDto => typeDto.NodeId);
|
||||
DataTypeDto? datatype = Database.FirstOrDefault<DataTypeDto>(sql);
|
||||
|
||||
// we cannot assign a data type if one was not found
|
||||
if (datatype != null)
|
||||
Attempt<int> dataTypeIdAttempt = _idKeyMap.Value.GetIdForKey(propertyType.DataTypeKey, UmbracoObjectTypes.DataType);
|
||||
if (dataTypeIdAttempt.Success)
|
||||
{
|
||||
propertyType.DataTypeId = datatype.NodeId;
|
||||
propertyType.DataTypeKey = datatype.NodeDto.UniqueId;
|
||||
propertyType.DataTypeId = dataTypeIdAttempt.Result;
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning(
|
||||
"Could not assign a data type for the property type {PropertyTypeAlias} since no data type was found with a property editor {PropertyEditorAlias}",
|
||||
propertyType.Alias, propertyType.PropertyEditorAlias);
|
||||
"Could not assign a data type for the property type {PropertyTypeAlias} since no integer Id was found matching the key {DataTypeKey}. Falling back to look up via the property editor alias.",
|
||||
propertyType.Alias,
|
||||
propertyType.DataTypeKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise if a property editor alias is provided, try to find a data type that uses that alias.
|
||||
if (propertyType.PropertyEditorAlias.IsNullOrWhiteSpace())
|
||||
{
|
||||
// We cannot try to assign a data type if it's empty.
|
||||
return;
|
||||
}
|
||||
|
||||
Sql<ISqlContext> sql = Sql()
|
||||
.Select<DataTypeDto>(dt => dt.Select(x => x.NodeDto))
|
||||
.From<DataTypeDto>()
|
||||
.InnerJoin<NodeDto>().On<DataTypeDto, NodeDto>((dt, n) => dt.NodeId == n.NodeId)
|
||||
.Where(
|
||||
"propertyEditorAlias = @propertyEditorAlias",
|
||||
new { propertyEditorAlias = propertyType.PropertyEditorAlias })
|
||||
.OrderBy<DataTypeDto>(typeDto => typeDto.NodeId);
|
||||
DataTypeDto? datatype = Database.FirstOrDefault<DataTypeDto>(sql);
|
||||
|
||||
// we cannot assign a data type if one was not found
|
||||
if (datatype != null)
|
||||
{
|
||||
propertyType.DataTypeId = datatype.NodeId;
|
||||
propertyType.DataTypeKey = datatype.NodeDto.UniqueId;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning(
|
||||
"Could not assign a data type for the property type {PropertyTypeAlias} since no data type was found with a property editor {PropertyEditorAlias}",
|
||||
propertyType.Alias,
|
||||
propertyType.PropertyEditorAlias);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract TEntity? PerformGet(string alias);
|
||||
|
||||
+4
-2
@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Querying;
|
||||
@@ -24,8 +25,9 @@ internal class MediaTypeRepository : ContentTypeRepositoryBase<IMediaType>, IMed
|
||||
ILogger<MediaTypeRepository> logger,
|
||||
IContentTypeCommonRepository commonRepository,
|
||||
ILanguageRepository languageRepository,
|
||||
IShortStringHelper shortStringHelper)
|
||||
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper)
|
||||
IShortStringHelper shortStringHelper,
|
||||
Lazy<IIdKeyMap> idKeyMap)
|
||||
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper, idKeyMap)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
+5
-3
@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence.Querying;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Factories;
|
||||
@@ -27,9 +28,10 @@ internal class MemberTypeRepository : ContentTypeRepositoryBase<IMemberType>, IM
|
||||
ILogger<MemberTypeRepository> logger,
|
||||
IContentTypeCommonRepository commonRepository,
|
||||
ILanguageRepository languageRepository,
|
||||
IShortStringHelper shortStringHelper)
|
||||
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper) =>
|
||||
_shortStringHelper = shortStringHelper;
|
||||
IShortStringHelper shortStringHelper,
|
||||
Lazy<IIdKeyMap> idKeyMap)
|
||||
: base(scopeAccessor, cache, logger, commonRepository, languageRepository, shortStringHelper, idKeyMap)
|
||||
=> _shortStringHelper = shortStringHelper;
|
||||
|
||||
protected override bool SupportsPublishing => MemberType.SupportsPublishingConst;
|
||||
|
||||
|
||||
@@ -1085,19 +1085,20 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0
|
||||
/// <inheritdoc />
|
||||
public void InvalidateSessionsForRemovedProviders(IEnumerable<string> currentLoginProviders)
|
||||
{
|
||||
// Get all the user or member keys associated with the removed providers.
|
||||
// Get all the user keys associated with the removed providers.
|
||||
Sql<ISqlContext> idsQuery = SqlContext.Sql()
|
||||
.Select<ExternalLoginDto>(x => x.UserOrMemberKey)
|
||||
.From<ExternalLoginDto>()
|
||||
.Where<ExternalLoginDto>(x => !x.LoginProvider.StartsWith(Constants.Security.MemberExternalAuthenticationTypePrefix)) // Only invalidate sessions relating to backoffice users, not members.
|
||||
.WhereNotIn<ExternalLoginDto>(x => x.LoginProvider, currentLoginProviders);
|
||||
List<Guid> userAndMemberKeysAssociatedWithRemovedProviders = Database.Fetch<Guid>(idsQuery);
|
||||
if (userAndMemberKeysAssociatedWithRemovedProviders.Count == 0)
|
||||
List<Guid> userKeysAssociatedWithRemovedProviders = Database.Fetch<Guid>(idsQuery);
|
||||
if (userKeysAssociatedWithRemovedProviders.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter for actual users and convert to integer IDs.
|
||||
var userIdsAssociatedWithRemovedProviders = userAndMemberKeysAssociatedWithRemovedProviders
|
||||
// Convert to user integer IDs.
|
||||
var userIdsAssociatedWithRemovedProviders = userKeysAssociatedWithRemovedProviders
|
||||
.Select(ConvertUserKeyToUserId)
|
||||
.Where(x => x.HasValue)
|
||||
.Select(x => x!.Value)
|
||||
@@ -1119,7 +1120,6 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0
|
||||
// User Ids are stored as integers in the umbracoUser table, but as a GUID representation
|
||||
// of that integer in umbracoExternalLogin (converted via IntExtensions.ToGuid()).
|
||||
// We need to parse that to get the user Ids to invalidate.
|
||||
// Note also that umbracoExternalLogin contains members too, as proper GUIDs, so we need to ignore them.
|
||||
IntExtensions.TryParseFromGuid(userOrMemberKey, out int? userId) ? userId : null;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -240,7 +240,7 @@ internal class Property : PublishedPropertyBase
|
||||
|
||||
EnsureSourceValuesInitialized();
|
||||
|
||||
var k = new CompositeStringStringKey(culture, segment);
|
||||
var k = new CompositeStringStringKey(culture ?? string.Empty, segment ?? string.Empty); // Null values are not valid when creating a CompositeStringStringKey.
|
||||
|
||||
SourceInterValue vvalue = _sourceValues!.GetOrAdd(k, _ =>
|
||||
new SourceInterValue
|
||||
|
||||
@@ -131,12 +131,17 @@ public class AuthenticationController : UmbracoApiControllerBase
|
||||
AuthorizationPolicies.BackOfficeAccess)] // Needed to enforce the principle set on the request, if one exists.
|
||||
public IDictionary<string, object> GetPasswordConfig(int userId)
|
||||
{
|
||||
if (HttpContext.HasActivePasswordResetFlowSession(userId))
|
||||
{
|
||||
return _passwordConfiguration.GetConfiguration();
|
||||
}
|
||||
|
||||
Attempt<int> currentUserId =
|
||||
_backofficeSecurityAccessor.BackOfficeSecurity?.GetUserId() ?? Attempt<int>.Fail();
|
||||
return _passwordConfiguration.GetConfiguration(
|
||||
currentUserId.Success
|
||||
? currentUserId.Result != userId
|
||||
: true);
|
||||
|
||||
return currentUserId.Success
|
||||
? _passwordConfiguration.GetConfiguration(currentUserId.Result != userId)
|
||||
: new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -417,6 +422,8 @@ public class AuthenticationController : UmbracoApiControllerBase
|
||||
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
|
||||
public async Task<ActionResult<UserDetail?>> PostLogin(LoginModel loginModel)
|
||||
{
|
||||
HttpContext.EndPasswordResetFlowSession();
|
||||
|
||||
// Start a timed scope to ensure failed responses return is a consistent time
|
||||
var loginDuration = Math.Max(_loginDurationAverage ?? _securitySettings.UserDefaultFailedLoginDurationInMilliseconds, _securitySettings.UserMinimumFailedLoginDurationInMilliseconds);
|
||||
await using var timedScope = new TimedScope(loginDuration, HttpContext.RequestAborted);
|
||||
@@ -490,6 +497,8 @@ public class AuthenticationController : UmbracoApiControllerBase
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
HttpContext.EndPasswordResetFlowSession();
|
||||
|
||||
BackOfficeIdentityUser? identityUser = await _userManager.FindByEmailAsync(model.Email);
|
||||
|
||||
await Task.Delay(RandomNumberGenerator.GetInt32(400, 2500)); // To randomize response time preventing user enumeration
|
||||
@@ -646,6 +655,8 @@ public class AuthenticationController : UmbracoApiControllerBase
|
||||
[AllowAnonymous]
|
||||
public async Task<IActionResult> PostSetPassword(SetPasswordModel model)
|
||||
{
|
||||
HttpContext.EndPasswordResetFlowSession();
|
||||
|
||||
BackOfficeIdentityUser? identityUser =
|
||||
await _userManager.FindByIdAsync(model.UserId.ToString(CultureInfo.InvariantCulture));
|
||||
if (identityUser is null)
|
||||
|
||||
@@ -402,6 +402,11 @@ public class BackOfficeController : UmbracoController
|
||||
|
||||
var result = await _userManager.VerifyUserTokenAsync(user, "Default", "ResetPassword", resetCode);
|
||||
|
||||
if (result)
|
||||
{
|
||||
HttpContext.StartPasswordResetFlowSession(userId);
|
||||
}
|
||||
|
||||
return result ?
|
||||
|
||||
// Redirect to login with userId and resetCode
|
||||
|
||||
@@ -5,9 +5,20 @@ namespace Umbraco.Extensions;
|
||||
|
||||
public static class HttpContextExtensions
|
||||
{
|
||||
private const string PasswordResetFlowSessionKey = nameof(PasswordResetFlowSessionKey);
|
||||
|
||||
public static void SetExternalLoginProviderErrors(this HttpContext httpContext, BackOfficeExternalLoginProviderErrors errors)
|
||||
=> httpContext.Items[nameof(BackOfficeExternalLoginProviderErrors)] = errors;
|
||||
|
||||
public static BackOfficeExternalLoginProviderErrors? GetExternalLoginProviderErrors(this HttpContext httpContext)
|
||||
=> httpContext.Items[nameof(BackOfficeExternalLoginProviderErrors)] as BackOfficeExternalLoginProviderErrors;
|
||||
|
||||
internal static void StartPasswordResetFlowSession(this HttpContext httpContext, int userId)
|
||||
=> httpContext.Session.SetInt32(PasswordResetFlowSessionKey, userId);
|
||||
|
||||
internal static void EndPasswordResetFlowSession(this HttpContext httpContext)
|
||||
=> httpContext.Session.Remove(PasswordResetFlowSessionKey);
|
||||
|
||||
internal static bool HasActivePasswordResetFlowSession(this HttpContext httpContext, int userId)
|
||||
=> httpContext.Session.GetInt32(PasswordResetFlowSessionKey) == userId;
|
||||
}
|
||||
|
||||
+5
-2
@@ -351,14 +351,17 @@ Use this directive to generate a thumbnail grid of media items.
|
||||
// sort function
|
||||
scope.sortBy = function (item) {
|
||||
if (scope.sortColumn === "updateDate") {
|
||||
return [-item['isFolder'],item['updateDate']];
|
||||
return [-item['isFolder'],parseUpdateDate(item['updateDate'])];
|
||||
}
|
||||
else {
|
||||
return [-item['isFolder'],item['name']];
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
function parseUpdateDate(date) {
|
||||
var parsedDate = Date.parse(date);
|
||||
return isNaN(parsedDate) ? date : parsedDate;
|
||||
}
|
||||
}
|
||||
|
||||
var directive = {
|
||||
|
||||
@@ -99,7 +99,7 @@ export default class UmbAuthElement extends LitElement {
|
||||
|
||||
@property({attribute: 'return-url'})
|
||||
set returnPath(value: string) {
|
||||
umbAuthContext.returnPath = value;
|
||||
umbAuthContext.returnPath = `${value}${encodeURIComponent(window.location.hash)}`;
|
||||
}
|
||||
get returnPath() {
|
||||
return umbAuthContext.returnPath;
|
||||
|
||||
@@ -74,7 +74,12 @@ export class UmbExternalLoginProviderElement extends LitElement {
|
||||
set externalLoginUrl(value: string) {
|
||||
const tempUrl = new URL(value, window.location.origin);
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
tempUrl.searchParams.append('redirectUrl', decodeURIComponent(searchParams.get('returnPath') ?? ''));
|
||||
let returnUrl = decodeURIComponent(searchParams.get('returnPath') ?? '');
|
||||
if(!returnUrl && window.location.hash) {
|
||||
returnUrl = `/umbraco${window.location.hash}`;
|
||||
}
|
||||
|
||||
tempUrl.searchParams.append('redirectUrl', returnUrl);
|
||||
this.#externalLoginUrl = tempUrl.pathname + tempUrl.search;
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,8 @@ public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase
|
||||
|
||||
protected IShortStringHelper ShortStringHelper => Services.GetRequiredService<IShortStringHelper>();
|
||||
|
||||
protected IIdKeyMap IdKeyMap => Services.GetRequiredService<IIdKeyMap>();
|
||||
|
||||
protected GlobalSettings GlobalSettings => Services.GetRequiredService<IOptions<GlobalSettings>>().Value;
|
||||
|
||||
protected IMapperCollection Mappers => Services.GetRequiredService<IMapperCollection>();
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ public class DocumentRepositoryTest : UmbracoIntegrationTest
|
||||
new ContentTypeCommonRepository(scopeAccessor, templateRepository, appCaches, ShortStringHelper);
|
||||
var languageRepository =
|
||||
new LanguageRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<LanguageRepository>());
|
||||
contentTypeRepository = new ContentTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<ContentTypeRepository>(), commonRepository, languageRepository, ShortStringHelper);
|
||||
contentTypeRepository = new ContentTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<ContentTypeRepository>(), commonRepository, languageRepository, ShortStringHelper, new Lazy<IIdKeyMap>(() => IdKeyMap));
|
||||
var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<RelationTypeRepository>());
|
||||
var entityRepository = new EntityRepository(scopeAccessor, AppCaches.Disabled);
|
||||
var relationRepository = new RelationRepository(scopeAccessor, LoggerFactory.CreateLogger<RelationRepository>(), relationTypeRepository, entityRepository);
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ public class MediaRepositoryTest : UmbracoIntegrationTest
|
||||
new ContentTypeCommonRepository(scopeAccessor, TemplateRepository, appCaches, ShortStringHelper);
|
||||
var languageRepository =
|
||||
new LanguageRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<LanguageRepository>());
|
||||
mediaTypeRepository = new MediaTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<MediaTypeRepository>(), commonRepository, languageRepository, ShortStringHelper);
|
||||
mediaTypeRepository = new MediaTypeRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<MediaTypeRepository>(), commonRepository, languageRepository, ShortStringHelper, new Lazy<IIdKeyMap>(() => IdKeyMap));
|
||||
var tagRepository = new TagRepository(scopeAccessor, appCaches, LoggerFactory.CreateLogger<TagRepository>());
|
||||
var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<RelationTypeRepository>());
|
||||
var entityRepository = new EntityRepository(scopeAccessor, AppCaches.Disabled);
|
||||
|
||||
+2
-1
@@ -9,6 +9,7 @@ using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
|
||||
using Umbraco.Cms.Infrastructure.Scoping;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
@@ -411,7 +412,7 @@ public class MediaTypeRepositoryTest : UmbracoIntegrationTest
|
||||
}
|
||||
|
||||
private MediaTypeRepository CreateRepository(IScopeProvider provider) =>
|
||||
new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger<MediaTypeRepository>(), CommonRepository, LanguageRepository, ShortStringHelper);
|
||||
new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger<MediaTypeRepository>(), CommonRepository, LanguageRepository, ShortStringHelper, new Lazy<IIdKeyMap>(() => IdKeyMap));
|
||||
|
||||
private EntityContainerRepository CreateContainerRepository(IScopeProvider provider) =>
|
||||
new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger<EntityContainerRepository>(), Constants.ObjectTypes.MediaTypeContainer);
|
||||
|
||||
+2
-1
@@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Persistence;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
|
||||
using Umbraco.Cms.Infrastructure.Scoping;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
@@ -26,7 +27,7 @@ public class MemberTypeRepositoryTest : UmbracoIntegrationTest
|
||||
{
|
||||
var commonRepository = GetRequiredService<IContentTypeCommonRepository>();
|
||||
var languageRepository = GetRequiredService<ILanguageRepository>();
|
||||
return new MemberTypeRepository((IScopeAccessor)provider, AppCaches.Disabled, Mock.Of<ILogger<MemberTypeRepository>>(), commonRepository, languageRepository, ShortStringHelper);
|
||||
return new MemberTypeRepository((IScopeAccessor)provider, AppCaches.Disabled, Mock.Of<ILogger<MemberTypeRepository>>(), commonRepository, languageRepository, ShortStringHelper, new Lazy<IIdKeyMap>(() => IdKeyMap));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
+1
-1
@@ -265,7 +265,7 @@ public class TemplateRepositoryTest : UmbracoIntegrationTest
|
||||
var commonRepository =
|
||||
new ContentTypeCommonRepository(scopeAccessor, templateRepository, AppCaches, ShortStringHelper);
|
||||
var languageRepository = new LanguageRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<LanguageRepository>());
|
||||
var contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<ContentTypeRepository>(), commonRepository, languageRepository, ShortStringHelper);
|
||||
var contentTypeRepository = new ContentTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<ContentTypeRepository>(), commonRepository, languageRepository, ShortStringHelper, new Lazy<IIdKeyMap>(() => IdKeyMap));
|
||||
var relationTypeRepository = new RelationTypeRepository(scopeAccessor, AppCaches.Disabled, LoggerFactory.CreateLogger<RelationTypeRepository>());
|
||||
var entityRepository = new EntityRepository(scopeAccessor, AppCaches.Disabled);
|
||||
var relationRepository = new RelationRepository(scopeAccessor, LoggerFactory.CreateLogger<RelationRepository>(), relationTypeRepository, entityRepository);
|
||||
|
||||
+59
@@ -1967,6 +1967,65 @@ public class ContentTypeServiceTests : UmbracoIntegrationTest
|
||||
.Variations);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Can_Create_Property_Type_Based_On_DataTypeKey()
|
||||
{
|
||||
// Arrange
|
||||
var cts = ContentTypeService;
|
||||
var dtdYesNo = DataTypeService.GetDataType(-49);
|
||||
IContentType ctBase = new ContentType(ShortStringHelper, -1)
|
||||
{
|
||||
Name = "Base",
|
||||
Alias = "Base",
|
||||
Icon = "folder.gif",
|
||||
Thumbnail = "folder.png"
|
||||
};
|
||||
ctBase.AddPropertyType(new PropertyType(ShortStringHelper, "ShouldNotMatter", ValueStorageType.Nvarchar)
|
||||
{
|
||||
Name = "Hide From Navigation",
|
||||
Alias = Constants.Conventions.Content.NaviHide,
|
||||
DataTypeKey = dtdYesNo.Key
|
||||
});
|
||||
cts.Save(ctBase);
|
||||
|
||||
// Assert
|
||||
ctBase = cts.Get(ctBase.Key);
|
||||
Assert.That(ctBase, Is.Not.Null);
|
||||
Assert.That(ctBase.HasIdentity, Is.True);
|
||||
Assert.That(ctBase.PropertyTypes.Count(), Is.EqualTo(1));
|
||||
Assert.That(ctBase.PropertyTypes.First().DataTypeId, Is.EqualTo(dtdYesNo.Id));
|
||||
Assert.That(ctBase.PropertyTypes.First().PropertyEditorAlias, Is.EqualTo(dtdYesNo.EditorAlias));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Can_Create_Property_Type_Based_On_PropertyEditorAlias()
|
||||
{
|
||||
// Arrange
|
||||
var cts = ContentTypeService;
|
||||
var dtdYesNo = DataTypeService.GetDataType(-49);
|
||||
IContentType ctBase = new ContentType(ShortStringHelper, -1)
|
||||
{
|
||||
Name = "Base",
|
||||
Alias = "Base",
|
||||
Icon = "folder.gif",
|
||||
Thumbnail = "folder.png"
|
||||
};
|
||||
ctBase.AddPropertyType(new PropertyType(ShortStringHelper, "Umbraco.TrueFalse", ValueStorageType.Nvarchar)
|
||||
{
|
||||
Name = "Hide From Navigation",
|
||||
Alias = Constants.Conventions.Content.NaviHide,
|
||||
});
|
||||
cts.Save(ctBase);
|
||||
|
||||
// Assert
|
||||
ctBase = cts.Get(ctBase.Key);
|
||||
Assert.That(ctBase, Is.Not.Null);
|
||||
Assert.That(ctBase.HasIdentity, Is.True);
|
||||
Assert.That(ctBase.PropertyTypes.Count(), Is.EqualTo(1));
|
||||
Assert.That(ctBase.PropertyTypes.First().DataTypeId, Is.EqualTo(dtdYesNo.Id));
|
||||
Assert.That(ctBase.PropertyTypes.First().PropertyEditorAlias, Is.EqualTo(dtdYesNo.EditorAlias));
|
||||
}
|
||||
|
||||
private ContentType CreateComponent()
|
||||
{
|
||||
var component = new ContentType(ShortStringHelper, -1)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Extensions;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Cache;
|
||||
|
||||
[TestFixture]
|
||||
public class DistributedCacheExtensionsTests
|
||||
{
|
||||
[Test]
|
||||
public void Member_GetPayloads_CorrectlyCreatesPayloads()
|
||||
{
|
||||
var members = new List<IMember>()
|
||||
{
|
||||
CreateMember(1, "Fred", "fred", "fred@test.com"),
|
||||
CreateMember(1, "Fred", "fred", "fred@test.com"),
|
||||
CreateMember(2, "Sally", "sally", "sally@test.com"),
|
||||
CreateMember(3, "Jane", "jane", "jane@test.com", "janeold"),
|
||||
};
|
||||
|
||||
var payloads = DistributedCacheExtensions.GetPayloads(members, false);
|
||||
Assert.AreEqual(3, payloads.Count());
|
||||
|
||||
var payloadForFred = payloads.First();
|
||||
Assert.AreEqual("fred", payloadForFred.Username);
|
||||
Assert.AreEqual(1, payloadForFred.Id);
|
||||
Assert.IsNull(payloadForFred.PreviousUsername);
|
||||
|
||||
var payloadForSally = payloads.Skip(1).First();
|
||||
Assert.AreEqual("sally", payloadForSally.Username);
|
||||
Assert.AreEqual(2, payloadForSally.Id);
|
||||
Assert.IsNull(payloadForSally.PreviousUsername);
|
||||
|
||||
var payloadForJane = payloads.Skip(2).First();
|
||||
Assert.AreEqual("jane", payloadForJane.Username);
|
||||
Assert.AreEqual(3, payloadForJane.Id);
|
||||
Assert.AreEqual("janeold", payloadForJane.PreviousUsername);
|
||||
}
|
||||
|
||||
private static IMember CreateMember(int id, string name, string username, string email, string? previousUserName = null)
|
||||
{
|
||||
var memberBuilder = new MemberBuilder()
|
||||
.AddMemberType()
|
||||
.Done()
|
||||
.WithId(id)
|
||||
.WithName(name)
|
||||
.WithLogin(username, "password")
|
||||
.WithEmail(email);
|
||||
|
||||
if (previousUserName != null)
|
||||
{
|
||||
memberBuilder.AddAdditionalData()
|
||||
.WithKeyValue(global::Umbraco.Cms.Core.Constants.Entities.AdditionalDataKeys.MemberPreviousUserName, previousUserName)
|
||||
.Done();
|
||||
}
|
||||
|
||||
return memberBuilder.Build();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
|
||||
"version": "13.10.0-rc",
|
||||
"version": "13.10.0",
|
||||
"assemblyVersion": {
|
||||
"precision": "build"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user