Compare commits

...
Author SHA1 Message Date
Andy Butland c2890e15c3 Bumped version to 13.10.0 2025-08-14 07:39:22 +01:00
Andy Butland a352c42742 Merge branch 'release/13.9.3' into release/13.10
# Conflicts:
#	version.json
2025-07-29 07:17:06 +02:00
9f37db18d1 Merge commit from fork
Co-authored-by: kjac <kja@umbraco.dk>
2025-07-29 05:10:52 +02:00
Andy ButlandandGitHub 59ad07209b Retrieve only user external logins when invalidate following removal of backoffice external user login (#19766)
* Retrieve only user external logins when invalidate following removal of backoffice external user login.

* Improved variable name.
2025-07-22 07:50:05 +00:00
Kenn JacobsenandGitHub 67abecc252 Add defensive coding to the member application initializer (#19760) 2025-07-21 12:24:51 +02:00
Andy Butland ebd0017f6e Bumped version to 13.9.3. 2025-07-21 11:53:33 +02:00
Andy ButlandandGitHub 417f15197e Parse update date before sorting in media list view (#19711)
* Parse update date before sorting in media list view.

* Moved function placement.
2025-07-16 09:45:17 +01:00
ce40103c4f Add support for programmatic creation of property types providing the data type key (#19720)
* Add support for programmatic creation of property types providing the data type key.

* Add integration tests

---------

Co-authored-by: kjac <kja@umbraco.dk>
2025-07-15 13:57:48 +02:00
Andy ButlandandGitHub 2748fdfc48 Adds variation by the header name Accept-Language to the delivery API output cache policy (#19709)
* Adds variation by the header name Accept-Language to the develivery API output cache policy

* Removed obsolete constructor (not necessary as the class is internal).

* Introduce contants for header names.
2025-07-11 15:51:05 +02:00
Andy ButlandandGitHub 53cc663bde Register no-op implementation of IMemberPartialViewCacheInvalidator in headless setups (#19666)
* Register no-op implementation of IMemberPartialViewCacheInvalidator in headless setups.

* Tidied usings.
2025-07-08 15:40:53 +02:00
Andy ButlandandGitHub 13a2cd71c4 Clear member cache by older user name when member user name is updated. (#19672)
* Clear member cache by older user name when member user name is updated.

* Added unit test.
2025-07-07 14:01:36 +02:00
kowsandGitHub a60ccd389b #16772 partial fix backoffice redirect after login (#19663)
* #16772 partial fix backoffice redirect after login

* #16772 partial fix backoffice OpenId redirect after login
2025-07-07 10:51:50 +02:00
Andy Butland fd95dc3915 Merge branch 'release/13.9.2' into v13/dev
# Conflicts:
#	version.json
2025-07-02 06:29:04 +02:00
Andy ButlandandGitHub 990e379ea8 Ensures that null values aren't used to create a CompositeStringStringKey (#19646)
Ensures that null values aren't used to create a CompositeStringStringKey.
2025-07-01 12:11:04 +00:00
Kenn JacobsenandGitHub b4144564c8 Merge commit from fork 2025-06-24 08:39:17 +02:00
Andy Butland f6dbe0f33e Bumped version to 13.9.2. 2025-06-10 08:33:39 +02:00
39 changed files with 444 additions and 107 deletions
@@ -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.",
@@ -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
@@ -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));
}
}
}
}
+31 -3
View File
@@ -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";
}
}
}
+14
View File
@@ -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);
}
}
+1 -1
View File
@@ -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));
+14 -3
View File
@@ -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);
@@ -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)
{
}
@@ -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);
@@ -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,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;
}
@@ -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 = {
+1 -1
View File
@@ -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>();
@@ -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);
@@ -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);
@@ -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);
@@ -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]
@@ -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);
@@ -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
View File
@@ -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"
},