Files
Umbraco-CMS/src/Umbraco.Core/Services/MemberContentEditingService.cs
Kenn Jacobsen 2cf28271cd Service refactoring to "fully" enable segments (#19114)
* Refactor serverside content editing to support all variance combinations

* Fix build errors

* Reintroduce the tests ignored by #19060

---------

Co-authored-by: Mads Rasmussen <madsr@hey.com>
2025-04-23 14:54:51 +02:00

153 lines
7.5 KiB
C#

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Core.Services;
internal sealed class MemberContentEditingService
: ContentEditingServiceBase<IMember, IMemberType, IMemberService, IMemberTypeService>, IMemberContentEditingService
{
private readonly ILogger<ContentEditingServiceBase<IMember, IMemberType, IMemberService, IMemberTypeService>> _logger;
private readonly IUserService _userService;
public MemberContentEditingService(
IMemberService contentService,
IMemberTypeService contentTypeService,
PropertyEditorCollection propertyEditorCollection,
IDataTypeService dataTypeService,
ILogger<ContentEditingServiceBase<IMember, IMemberType, IMemberService, IMemberTypeService>> logger,
ICoreScopeProvider scopeProvider,
IUserIdKeyResolver userIdKeyResolver,
IMemberValidationService memberValidationService,
IUserService userService,
IOptionsMonitor<ContentSettings> optionsMonitor,
IRelationService relationService)
: base(contentService, contentTypeService, propertyEditorCollection, dataTypeService, logger, scopeProvider, userIdKeyResolver, memberValidationService, optionsMonitor, relationService)
{
_logger = logger;
_userService = userService;
}
public async Task<Attempt<ContentValidationResult, ContentEditingOperationStatus>> ValidateAsync(MemberEditingModelBase editingModel, Guid memberTypeKey)
=> await ValidatePropertiesAsync(editingModel, memberTypeKey);
public async Task<Attempt<MemberUpdateResult, ContentEditingOperationStatus>> UpdateAsync(IMember member, MemberEditingModelBase updateModel, Guid userKey)
{
IMemberType memberType = await ContentTypeService.GetAsync(member.ContentType.Key)
?? throw new InvalidOperationException($"The member type {member.ContentType.Alias} could not be found.");
IUser user = await _userService.GetRequiredUserAsync(userKey);
if (ValidateAccessToSensitiveProperties(member, memberType, updateModel, user) is false)
{
return Attempt.FailWithStatus(ContentEditingOperationStatus.NotAllowed, new MemberUpdateResult());
}
// store any sensitive properties that must be explicitly carried over between updates (due to missing user access to sensitive data)
Dictionary<string, object?> sensitivePropertiesToRetain = FindSensitivePropertiesToRetain(member, memberType, user);
Attempt<MemberUpdateResult, ContentEditingOperationStatus> result = await MapUpdate<MemberUpdateResult>(member, updateModel);
if (result.Success == false)
{
return Attempt.FailWithStatus(result.Status, result.Result);
}
// restore the retained sensitive properties after the base update (they have been removed from the member at this point)
RetainSensitiveProperties(member, sensitivePropertiesToRetain);
// the create mapping might succeed, but this doesn't mean the model is valid at property level.
// we'll return the actual property validation status if the entire operation succeeds.
ContentEditingOperationStatus validationStatus = result.Status;
ContentValidationResult validationResult = result.Result.ValidationResult;
var currentUserId = await GetUserIdAsync(userKey);
ContentEditingOperationStatus operationStatus = Save(member, currentUserId);
return operationStatus == ContentEditingOperationStatus.Success
? Attempt.SucceedWithStatus(validationStatus, new MemberUpdateResult { Content = member, ValidationResult = validationResult })
: Attempt.FailWithStatus(operationStatus, new MemberUpdateResult { Content = member });
}
public async Task<Attempt<IMember?, ContentEditingOperationStatus>> DeleteAsync(Guid key, Guid userKey)
=> await HandleDeleteAsync(key, userKey, false);
protected override IMember New(string? name, int parentId, IMemberType memberType)
=> throw new NotSupportedException("Member creation is not supported by this service. This should never be called.");
protected override OperationResult? Move(IMember member, int newParentId, int userId)
=> throw new InvalidOperationException("Move is not supported for members");
protected override IMember? Copy(IMember member, int newParentId, bool relateToOriginal, bool includeDescendants, int userId)
=> throw new NotSupportedException("Copy is not supported for Member");
protected override OperationResult? MoveToRecycleBin(IMember member, int userId)
=> throw new InvalidOperationException("Recycle bin is not supported for members");
protected override OperationResult? Delete(IMember member, int userId)
=> ContentService.Delete(member, userId).Result;
private ContentEditingOperationStatus Save(IMember member, int userId)
{
try
{
Attempt<OperationResult?> saveResult = ContentService.Save(member, userId);
return saveResult.Result?.Result switch
{
// these are the only result states currently expected from Save
OperationResultType.Success => ContentEditingOperationStatus.Success,
OperationResultType.FailedCancelledByEvent => ContentEditingOperationStatus.CancelledByNotification,
// for any other state we'll return "unknown" so we know that we need to amend this
_ => ContentEditingOperationStatus.Unknown
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Member save operation failed");
return ContentEditingOperationStatus.Unknown;
}
}
private bool ValidateAccessToSensitiveProperties(IMember member, IMemberType memberType, MemberEditingModelBase updateModel, IUser user)
{
if (user.HasAccessToSensitiveData())
{
return true;
}
var sensitivePropertyAliases = memberType.GetSensitivePropertyTypeAliases().ToArray();
return updateModel
.Properties
.Select(property => property.Alias)
.Intersect(sensitivePropertyAliases, StringComparer.OrdinalIgnoreCase)
.Any() is false;
}
private Dictionary<string, object?> FindSensitivePropertiesToRetain(IMember member, IMemberType memberType, IUser user)
{
if (user.HasAccessToSensitiveData())
{
return new Dictionary<string, object?>();
}
var sensitivePropertyAliases = memberType.GetSensitivePropertyTypeAliases().ToArray();
// NOTE: this is explicitly NOT handling variance. if variance becomes a thing for members, this needs to be amended.
return sensitivePropertyAliases.ToDictionary(alias => alias, alias => member.GetValue(alias));
}
private void RetainSensitiveProperties(IMember member, Dictionary<string, object?> sensitivePropertyValues)
{
foreach (KeyValuePair<string, object?> sensitiveProperty in sensitivePropertyValues)
{
// NOTE: this is explicitly NOT handling variance. if variance becomes a thing for members, this needs to be amended.
member.SetValue(sensitiveProperty.Key, sensitiveProperty.Value);
}
}
}