Merge remote-tracking branch 'origin/netcore/dev' into netcore/feature/align-infrastructure-namespaces
# Conflicts: # src/Umbraco.Infrastructure/Cache/DistributedCacheBinder_Handlers.cs # src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.MappingProfiles.cs # src/Umbraco.Infrastructure/PropertyEditors/PropertyEditorsComponent.cs # src/Umbraco.Infrastructure/Security/BackOfficeClaimsPrincipalFactory.cs # src/Umbraco.Infrastructure/Security/IBackOfficeUserManager.cs # src/Umbraco.Infrastructure/Security/IUmbracoUserManager.cs # src/Umbraco.Infrastructure/Security/SignOutAuditEventArgs.cs # src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs # src/Umbraco.Infrastructure/Security/UserInviteEventArgs.cs # src/Umbraco.Tests.UnitTests/AutoFixture/AutoMoqDataAttribute.cs # src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackOffice/BackOfficeLookupNormalizerTests.cs # src/Umbraco.Web.BackOffice/Controllers/MemberController.cs # src/Umbraco.Web/Security/IBackOfficeUserPasswordChecker.cs # src/Umbraco.Web/Security/Providers/MembersRoleProvider.cs
This commit is contained in:
@@ -70,10 +70,18 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
/// </summary>
|
||||
protected ILocalizedTextService LocalizedTextService { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Handles if the content for the specified ID isn't found
|
||||
/// </summary>
|
||||
/// <param name="id">The content ID to find</param>
|
||||
/// <param name="throwException">Whether to throw an exception</param>
|
||||
/// <returns>The error response</returns>
|
||||
protected NotFoundObjectResult HandleContentNotFound(object id)
|
||||
{
|
||||
ModelState.AddModelError("id", $"content with id: {id} was not found");
|
||||
var errorResponse = NotFound(ModelState);
|
||||
NotFoundObjectResult errorResponse = NotFound(ModelState);
|
||||
|
||||
|
||||
return errorResponse;
|
||||
}
|
||||
|
||||
@@ -90,7 +98,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
where TSaved : IContentSave<TPersisted>
|
||||
{
|
||||
// map the property values
|
||||
foreach (var propertyDto in dto.Properties)
|
||||
foreach (ContentPropertyDto propertyDto in dto.Properties)
|
||||
{
|
||||
// get the property editor
|
||||
if (propertyDto.PropertyEditor == null)
|
||||
@@ -101,42 +109,53 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
|
||||
// get the value editor
|
||||
// nothing to save/map if it is readonly
|
||||
var valueEditor = propertyDto.PropertyEditor.GetValueEditor();
|
||||
if (valueEditor.IsReadOnly) continue;
|
||||
IDataValueEditor valueEditor = propertyDto.PropertyEditor.GetValueEditor();
|
||||
if (valueEditor.IsReadOnly)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// get the property
|
||||
var property = contentItem.PersistedContent.Properties[propertyDto.Alias];
|
||||
IProperty property = contentItem.PersistedContent.Properties[propertyDto.Alias];
|
||||
|
||||
// prepare files, if any matching property and culture
|
||||
var files = contentItem.UploadedFiles
|
||||
ContentPropertyFile[] files = contentItem.UploadedFiles
|
||||
.Where(x => x.PropertyAlias == propertyDto.Alias && x.Culture == propertyDto.Culture && x.Segment == propertyDto.Segment)
|
||||
.ToArray();
|
||||
|
||||
foreach (var file in files)
|
||||
foreach (ContentPropertyFile file in files)
|
||||
{
|
||||
file.FileName = file.FileName.ToSafeFileName(ShortStringHelper);
|
||||
}
|
||||
|
||||
// create the property data for the property editor
|
||||
var data = new ContentPropertyData(propertyDto.Value, propertyDto.DataType.Configuration)
|
||||
{
|
||||
ContentKey = contentItem.PersistedContent.Key,
|
||||
PropertyTypeKey = property.PropertyType.Key,
|
||||
Files = files
|
||||
Files = files
|
||||
};
|
||||
|
||||
// let the editor convert the value that was received, deal with files, etc
|
||||
var value = valueEditor.FromEditor(data, getPropertyValue(contentItem, property));
|
||||
object value = valueEditor.FromEditor(data, getPropertyValue(contentItem, property));
|
||||
|
||||
// set the value - tags are special
|
||||
var tagAttribute = propertyDto.PropertyEditor.GetTagAttribute();
|
||||
TagsPropertyEditorAttribute tagAttribute = propertyDto.PropertyEditor.GetTagAttribute();
|
||||
if (tagAttribute != null)
|
||||
{
|
||||
var tagConfiguration = ConfigurationEditor.ConfigurationAs<TagConfiguration>(propertyDto.DataType.Configuration);
|
||||
if (tagConfiguration.Delimiter == default) tagConfiguration.Delimiter = tagAttribute.Delimiter;
|
||||
TagConfiguration tagConfiguration = ConfigurationEditor.ConfigurationAs<TagConfiguration>(propertyDto.DataType.Configuration);
|
||||
if (tagConfiguration.Delimiter == default)
|
||||
{
|
||||
tagConfiguration.Delimiter = tagAttribute.Delimiter;
|
||||
}
|
||||
|
||||
var tagCulture = property.PropertyType.VariesByCulture() ? culture : null;
|
||||
property.SetTagsValue(_serializer, value, tagConfiguration, tagCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
savePropertyValue(contentItem, property, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,38 +172,45 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
/// </remarks>
|
||||
protected TPersisted GetObjectFromRequest<TPersisted>(Func<TPersisted> getFromService)
|
||||
{
|
||||
//checks if the request contains the key and the item is not null, if that is the case, return it from the request, otherwise return
|
||||
// checks if the request contains the key and the item is not null, if that is the case, return it from the request, otherwise return
|
||||
// it from the callback
|
||||
return HttpContext.Items.ContainsKey(typeof(TPersisted).ToString()) && HttpContext.Items[typeof(TPersisted).ToString()] != null
|
||||
? (TPersisted) HttpContext.Items[typeof (TPersisted).ToString()]
|
||||
? (TPersisted)HttpContext.Items[typeof(TPersisted).ToString()]
|
||||
: getFromService();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the action passed in means we need to create something new
|
||||
/// </summary>
|
||||
/// <param name="action"></param>
|
||||
/// <returns></returns>
|
||||
internal static bool IsCreatingAction(ContentSaveAction action)
|
||||
{
|
||||
return (action.ToString().EndsWith("New"));
|
||||
}
|
||||
/// <param name="action">The content action</param>
|
||||
/// <returns>Returns true if this is a creating action</returns>
|
||||
internal static bool IsCreatingAction(ContentSaveAction action) => action.ToString().EndsWith("New");
|
||||
|
||||
protected void AddCancelMessage(INotificationModel display,
|
||||
string header = "speechBubbles/operationCancelledHeader",
|
||||
string message = "speechBubbles/operationCancelledText",
|
||||
bool localizeHeader = true,
|
||||
/// <summary>
|
||||
/// Adds a cancelled message to the display
|
||||
/// </summary>
|
||||
/// <param name="display"></param>
|
||||
/// <param name="header"></param>
|
||||
/// <param name="message"></param>
|
||||
/// <param name="localizeHeader"></param>
|
||||
/// <param name="localizeMessage"></param>
|
||||
/// <param name="headerParams"></param>
|
||||
/// <param name="messageParams"></param>
|
||||
protected void AddCancelMessage(INotificationModel display, string header = "speechBubbles/operationCancelledHeader", string message = "speechBubbles/operationCancelledText", bool localizeHeader = true,
|
||||
bool localizeMessage = true,
|
||||
string[] headerParams = null,
|
||||
string[] messageParams = null)
|
||||
{
|
||||
//if there's already a default event message, don't add our default one
|
||||
var msgs = EventMessages;
|
||||
if (msgs != null && msgs.GetOrDefault().GetAll().Any(x => x.IsDefaultEventMessage)) return;
|
||||
// if there's already a default event message, don't add our default one
|
||||
IEventMessagesFactory messages = EventMessages;
|
||||
if (messages != null && messages.GetOrDefault().GetAll().Any(x => x.IsDefaultEventMessage))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
display.AddWarningNotification(
|
||||
localizeHeader ? LocalizedTextService.Localize(header, headerParams) : header,
|
||||
localizeMessage ? LocalizedTextService.Localize(message, messageParams): message);
|
||||
localizeMessage ? LocalizedTextService.Localize(message, messageParams) : message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.ContentApps;
|
||||
@@ -19,6 +19,7 @@ using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
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.Security;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
@@ -46,46 +47,72 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
[OutgoingNoHyphenGuidFormat]
|
||||
public class MemberController : ContentControllerBase
|
||||
{
|
||||
private readonly MemberPasswordConfigurationSettings _passwordConfig;
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
private readonly LegacyPasswordSecurity _passwordSecurity;
|
||||
private readonly UmbracoMapper _umbracoMapper;
|
||||
private readonly IMemberService _memberService;
|
||||
private readonly IMemberTypeService _memberTypeService;
|
||||
private readonly IMemberManager _memberManager;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor;
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MemberController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="cultureDictionary">The culture dictionary</param>
|
||||
/// <param name="loggerFactory">The logger factory</param>
|
||||
/// <param name="shortStringHelper">The string helper</param>
|
||||
/// <param name="eventMessages">The event messages factory</param>
|
||||
/// <param name="localizedTextService">The entry point for localizing key services</param>
|
||||
/// <param name="propertyEditors">The property editors</param>
|
||||
/// <param name="umbracoMapper">The mapper</param>
|
||||
/// <param name="memberService">The member service</param>
|
||||
/// <param name="memberTypeService">The member type service</param>
|
||||
/// <param name="memberManager">The member manager</param>
|
||||
/// <param name="dataTypeService">The data-type service</param>
|
||||
/// <param name="backOfficeSecurityAccessor">The back office security accessor</param>
|
||||
/// <param name="jsonSerializer">The JSON serializer</param>
|
||||
public MemberController(
|
||||
ICultureDictionary cultureDictionary,
|
||||
ILoggerFactory loggerFactory,
|
||||
IShortStringHelper shortStringHelper,
|
||||
IEventMessagesFactory eventMessages,
|
||||
ILocalizedTextService localizedTextService,
|
||||
IOptions<MemberPasswordConfigurationSettings> passwordConfig,
|
||||
PropertyEditorCollection propertyEditors,
|
||||
LegacyPasswordSecurity passwordSecurity,
|
||||
UmbracoMapper umbracoMapper,
|
||||
IMemberService memberService,
|
||||
IMemberTypeService memberTypeService,
|
||||
IMemberManager memberManager,
|
||||
IDataTypeService dataTypeService,
|
||||
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IJsonSerializer jsonSerializer)
|
||||
: base(cultureDictionary, loggerFactory, shortStringHelper, eventMessages, localizedTextService, jsonSerializer)
|
||||
{
|
||||
_passwordConfig = passwordConfig.Value;
|
||||
_propertyEditors = propertyEditors;
|
||||
_passwordSecurity = passwordSecurity;
|
||||
_umbracoMapper = umbracoMapper;
|
||||
_memberService = memberService;
|
||||
_memberTypeService = memberTypeService;
|
||||
_memberManager = memberManager;
|
||||
_dataTypeService = dataTypeService;
|
||||
_localizedTextService = localizedTextService;
|
||||
_backofficeSecurityAccessor = backofficeSecurityAccessor;
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_shortStringHelper = shortStringHelper;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The paginated list of members
|
||||
/// </summary>
|
||||
/// <param name="pageNumber">The page number to display</param>
|
||||
/// <param name="pageSize">The size of the page</param>
|
||||
/// <param name="orderBy">The ordering of the member list</param>
|
||||
/// <param name="orderDirection">The direction of the member list</param>
|
||||
/// <param name="orderBySystemField">The system field to order by</param>
|
||||
/// <param name="filter">The current filter for the list</param>
|
||||
/// <param name="memberTypeAlias">The member type</param>
|
||||
/// <returns>The paged result of members</returns>
|
||||
public PagedResult<MemberBasic> GetPagedResults(
|
||||
int pageNumber = 1,
|
||||
int pageSize = 100,
|
||||
@@ -101,8 +128,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero");
|
||||
}
|
||||
|
||||
var members = _memberService
|
||||
.GetAll((pageNumber - 1), pageSize, out var totalRecords, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter).ToArray();
|
||||
IMember[] members = _memberService.GetAll(
|
||||
pageNumber - 1,
|
||||
pageSize,
|
||||
out var totalRecords,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
orderBySystemField,
|
||||
memberTypeAlias,
|
||||
filter).ToArray();
|
||||
if (totalRecords == 0)
|
||||
{
|
||||
return new PagedResult<MemberBasic>(0, 0, 0);
|
||||
@@ -110,8 +144,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
|
||||
var pagedResult = new PagedResult<MemberBasic>(totalRecords, pageNumber, pageSize)
|
||||
{
|
||||
Items = members
|
||||
.Select(x => _umbracoMapper.Map<MemberBasic>(x))
|
||||
Items = members.Select(x => _umbracoMapper.Map<MemberBasic>(x))
|
||||
};
|
||||
return pagedResult;
|
||||
}
|
||||
@@ -119,15 +152,15 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
/// <summary>
|
||||
/// Returns a display node with a list view to render members
|
||||
/// </summary>
|
||||
/// <param name="listName"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="listName">The member type to list</param>
|
||||
/// <returns>The member list for display</returns>
|
||||
public MemberListDisplay GetListNodeDisplay(string listName)
|
||||
{
|
||||
var foundType = _memberTypeService.Get(listName);
|
||||
var name = foundType != null ? foundType.Name : listName;
|
||||
IMemberType foundType = _memberTypeService.Get(listName);
|
||||
string name = foundType != null ? foundType.Name : listName;
|
||||
|
||||
var apps = new List<ContentApp>();
|
||||
apps.Add(ListViewContentAppFactory.CreateContentApp(_dataTypeService, _propertyEditors, listName, "member", Constants.DataTypes.DefaultMembersListView));
|
||||
apps.Add(ListViewContentAppFactory.CreateContentApp(_dataTypeService, _propertyEditors, listName, "member", Core.Constants.DataTypes.DefaultMembersListView));
|
||||
apps[0].Active = true;
|
||||
|
||||
var display = new MemberListDisplay
|
||||
@@ -148,138 +181,130 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
/// <summary>
|
||||
/// Gets the content json for the member
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="key">The Guid key of the member</param>
|
||||
/// <returns>The member for display</returns>
|
||||
[OutgoingEditorModelEvent]
|
||||
public MemberDisplay GetByKey(Guid key)
|
||||
{
|
||||
var foundMember = _memberService.GetByKey(key);
|
||||
IMember foundMember = _memberService.GetByKey(key);
|
||||
if (foundMember == null)
|
||||
{
|
||||
HandleContentNotFound(key);
|
||||
}
|
||||
|
||||
return _umbracoMapper.Map<MemberDisplay>(foundMember);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets an empty content item for the
|
||||
/// </summary>
|
||||
/// <param name="contentTypeAlias"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="contentTypeAlias">The content type</param>
|
||||
/// <returns>The empty member for display</returns>
|
||||
[OutgoingEditorModelEvent]
|
||||
public ActionResult<MemberDisplay> GetEmpty(string contentTypeAlias = null)
|
||||
{
|
||||
IMember emptyContent;
|
||||
if (contentTypeAlias == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var contentType = _memberTypeService.Get(contentTypeAlias);
|
||||
IMemberType contentType = _memberTypeService.Get(contentTypeAlias);
|
||||
if (contentType == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var passwordGenerator = new PasswordGenerator(_passwordConfig);
|
||||
string newPassword = _memberManager.GeneratePassword();
|
||||
|
||||
emptyContent = new Member(contentType);
|
||||
emptyContent.AdditionalData["NewPassword"] = passwordGenerator.GeneratePassword();
|
||||
IMember emptyContent = new Member(contentType);
|
||||
emptyContent.AdditionalData["NewPassword"] = newPassword;
|
||||
return _umbracoMapper.Map<MemberDisplay>(emptyContent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Saves member
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
/// <param name="contentItem">The content item to save as a member</param>
|
||||
/// <returns>The resulting member display object</returns>
|
||||
[FileUploadCleanupFilter]
|
||||
[OutgoingEditorModelEvent]
|
||||
[MemberSaveValidation]
|
||||
public async Task<ActionResult<MemberDisplay>> PostSave(
|
||||
[ModelBinder(typeof(MemberBinder))]
|
||||
MemberSave contentItem)
|
||||
public async Task<ActionResult<MemberDisplay>> PostSave([ModelBinder(typeof(MemberBinder))] MemberSave contentItem)
|
||||
{
|
||||
if (contentItem == null)
|
||||
{
|
||||
throw new ArgumentNullException("The member content item was null");
|
||||
}
|
||||
|
||||
//If we've reached here it means:
|
||||
// If we've reached here it means:
|
||||
// * Our model has been bound
|
||||
// * and validated
|
||||
// * any file attachments have been saved to their temporary location for us to use
|
||||
// * we have a reference to the DTO object and the persisted object
|
||||
// * Permissions are valid
|
||||
|
||||
//map the properties to the persisted entity
|
||||
// map the properties to the persisted entity
|
||||
MapPropertyValues(contentItem);
|
||||
|
||||
await ValidateMemberDataAsync(contentItem);
|
||||
|
||||
//Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors
|
||||
// Unlike content/media - if there are errors for a member, we do NOT proceed to save them, we cannot so return the errors
|
||||
if (ModelState.IsValid == false)
|
||||
{
|
||||
var forDisplay = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
|
||||
MemberDisplay forDisplay = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
|
||||
forDisplay.Errors = ModelState.ToErrorDictionary();
|
||||
return new ValidationErrorResult(forDisplay);
|
||||
}
|
||||
|
||||
//We're gonna look up the current roles now because the below code can cause
|
||||
// events to be raised and developers could be manually adding roles to members in
|
||||
// their handlers. If we don't look this up now there's a chance we'll just end up
|
||||
// removing the roles they've assigned.
|
||||
var currRoles = _memberService.GetAllRoles(contentItem.PersistedContent.Username);
|
||||
//find the ones to remove and remove them
|
||||
var rolesToRemove = currRoles.Except(contentItem.Groups).ToArray();
|
||||
|
||||
//Depending on the action we need to first do a create or update using the membership provider
|
||||
// Depending on the action we need to first do a create or update using the membership manager
|
||||
// this ensures that passwords are formatted correctly and also performs the validation on the provider itself.
|
||||
|
||||
switch (contentItem.Action)
|
||||
{
|
||||
case ContentSaveAction.Save:
|
||||
UpdateMemberData(contentItem);
|
||||
ActionResult<bool> updateSuccessful = await UpdateMemberAsync(contentItem);
|
||||
if (!(updateSuccessful.Result is null))
|
||||
{
|
||||
return updateSuccessful.Result;
|
||||
}
|
||||
|
||||
break;
|
||||
case ContentSaveAction.SaveNew:
|
||||
contentItem.PersistedContent = CreateMemberData(contentItem);
|
||||
ActionResult<bool> createSuccessful = await CreateMemberAsync(contentItem);
|
||||
if (!(createSuccessful.Result is null))
|
||||
{
|
||||
return createSuccessful.Result;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
//we don't support anything else for members
|
||||
// we don't support anything else for members
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
//TODO: There's 3 things saved here and we should do this all in one transaction, which we can do here by wrapping in a scope
|
||||
// TODO: There's 3 things saved here and we should do this all in one transaction, which we can do here by wrapping in a scope
|
||||
// but it would be nicer to have this taken care of within the Save method itself
|
||||
|
||||
//create/save the IMember
|
||||
_memberService.Save(contentItem.PersistedContent);
|
||||
// return the updated model
|
||||
MemberDisplay display = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
|
||||
|
||||
//Now let's do the role provider stuff - now that we've saved the content item (that is important since
|
||||
// if we are changing the username, it must be persisted before looking up the member roles).
|
||||
if (rolesToRemove.Any())
|
||||
{
|
||||
_memberService.DissociateRoles(new[] { contentItem.PersistedContent.Username }, rolesToRemove);
|
||||
}
|
||||
//find the ones to add and add them
|
||||
var toAdd = contentItem.Groups.Except(currRoles).ToArray();
|
||||
if (toAdd.Any())
|
||||
{
|
||||
//add the ones submitted
|
||||
_memberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd);
|
||||
}
|
||||
|
||||
//return the updated model
|
||||
var display = _umbracoMapper.Map<MemberDisplay>(contentItem.PersistedContent);
|
||||
|
||||
//lastly, if it is not valid, add the model state to the outgoing object and throw a 403
|
||||
// lastly, if it is not valid, add the model state to the outgoing object and throw a 403
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
display.Errors = ModelState.ToErrorDictionary();
|
||||
return new ValidationErrorResult(display, StatusCodes.Status403Forbidden);
|
||||
}
|
||||
|
||||
var localizedTextService = _localizedTextService;
|
||||
//put the correct messages in
|
||||
ILocalizedTextService localizedTextService = _localizedTextService;
|
||||
|
||||
// put the correct messages in
|
||||
switch (contentItem.Action)
|
||||
{
|
||||
case ContentSaveAction.Save:
|
||||
case ContentSaveAction.SaveNew:
|
||||
display.AddSuccessNotification(localizedTextService.Localize("speechBubbles/editMemberSaved"), localizedTextService.Localize("speechBubbles/editMemberSaved"));
|
||||
display.AddSuccessNotification(
|
||||
localizedTextService.Localize("speechBubbles/editMemberSaved"),
|
||||
localizedTextService.Localize("speechBubbles/editMemberSaved"));
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -289,81 +314,121 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
/// <summary>
|
||||
/// Maps the property values to the persisted entity
|
||||
/// </summary>
|
||||
/// <param name="contentItem"></param>
|
||||
/// <param name="contentItem">The member content item to map properties from</param>
|
||||
private void MapPropertyValues(MemberSave contentItem)
|
||||
{
|
||||
UpdateName(contentItem);
|
||||
// Don't update the name if it is empty
|
||||
if (contentItem.Name.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
contentItem.PersistedContent.Name = contentItem.Name;
|
||||
}
|
||||
|
||||
//map the custom properties - this will already be set for new entities in our member binder
|
||||
// map the custom properties - this will already be set for new entities in our member binder
|
||||
contentItem.PersistedContent.Email = contentItem.Email;
|
||||
contentItem.PersistedContent.Username = contentItem.Username;
|
||||
|
||||
//use the base method to map the rest of the properties
|
||||
base.MapPropertyValuesForPersistence<IMember, MemberSave>(
|
||||
// use the base method to map the rest of the properties
|
||||
MapPropertyValuesForPersistence<IMember, MemberSave>(
|
||||
contentItem,
|
||||
contentItem.PropertyCollectionDto,
|
||||
(save, property) => property.GetValue(), //get prop val
|
||||
(save, property, v) => property.SetValue(v), //set prop val
|
||||
(save, property) => property.GetValue(), // get prop val
|
||||
(save, property, v) => property.SetValue(v), // set prop val
|
||||
null); // member are all invariant
|
||||
}
|
||||
|
||||
private IMember CreateMemberData(MemberSave contentItem)
|
||||
/// <summary>
|
||||
/// Create a member from the supplied member content data
|
||||
///
|
||||
/// All member password processing and creation is done via the identity manager
|
||||
/// </summary>
|
||||
/// <param name="contentItem">Member content data</param>
|
||||
/// <returns>The identity result of the created member</returns>
|
||||
private async Task<ActionResult<bool>> CreateMemberAsync(MemberSave contentItem)
|
||||
{
|
||||
throw new NotImplementedException("Members have not been migrated to netcore");
|
||||
IMemberType memberType = _memberTypeService.Get(contentItem.ContentTypeAlias);
|
||||
if (memberType == null)
|
||||
{
|
||||
throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}");
|
||||
}
|
||||
|
||||
// TODO: all member password processing and creation needs to be done with a new aspnet identity MemberUserManager that hasn't been created yet.
|
||||
var identityMember = MembersIdentityUser.CreateNew(
|
||||
contentItem.Username,
|
||||
contentItem.Email,
|
||||
memberType.Alias,
|
||||
contentItem.Name);
|
||||
|
||||
//var memberType = _memberTypeService.Get(contentItem.ContentTypeAlias);
|
||||
//if (memberType == null)
|
||||
// throw new InvalidOperationException($"No member type found with alias {contentItem.ContentTypeAlias}");
|
||||
//var member = new Member(contentItem.Name, contentItem.Email, contentItem.Username, memberType, true)
|
||||
//{
|
||||
// CreatorId = _backofficeSecurityAccessor.BackofficeSecurity.CurrentUser.Id,
|
||||
// RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword),
|
||||
// Comments = contentItem.Comments,
|
||||
// IsApproved = contentItem.IsApproved
|
||||
//};
|
||||
IdentityResult created = await _memberManager.CreateAsync(identityMember, contentItem.Password.NewPassword);
|
||||
|
||||
//return member;
|
||||
if (created.Succeeded == false)
|
||||
{
|
||||
return new ValidationErrorResult(created.Errors.ToErrorMessage());
|
||||
}
|
||||
|
||||
// now re-look up the member, which will now exist
|
||||
IMember member = _memberService.GetByEmail(contentItem.Email);
|
||||
|
||||
// map the save info over onto the user
|
||||
member = _umbracoMapper.Map<MemberSave, IMember>(contentItem, member);
|
||||
|
||||
int creatorId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
|
||||
member.CreatorId = creatorId;
|
||||
|
||||
// assign the mapped property values that are not part of the identity properties
|
||||
string[] builtInAliases = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper).Select(x => x.Key).ToArray();
|
||||
foreach (ContentPropertyBasic property in contentItem.Properties)
|
||||
{
|
||||
if (builtInAliases.Contains(property.Alias) == false)
|
||||
{
|
||||
member.Properties[property.Alias].SetValue(property.Value);
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: do we need to resave the key?
|
||||
//contentItem.PersistedContent.Key = contentItem.Key;
|
||||
|
||||
// now the member has been saved via identity, resave the member with mapped content properties
|
||||
_memberService.Save(member);
|
||||
contentItem.PersistedContent = member;
|
||||
|
||||
await AddOrUpdateRoles(contentItem, identityMember);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update the member security data
|
||||
/// </summary>
|
||||
/// <param name="contentItem"></param>
|
||||
/// <returns>
|
||||
/// If the password has been reset then this method will return the reset/generated password, otherwise will return null.
|
||||
/// </returns>
|
||||
private void UpdateMemberData(MemberSave contentItem)
|
||||
/// </summary>
|
||||
/// <param name="contentItem">The member to save</param>
|
||||
private async Task<ActionResult<bool>> UpdateMemberAsync(MemberSave contentItem)
|
||||
{
|
||||
contentItem.PersistedContent.WriterId = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
|
||||
contentItem.PersistedContent.WriterId = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Id;
|
||||
|
||||
// If the user doesn't have access to sensitive values, then we need to check if any of the built in member property types
|
||||
// have been marked as sensitive. If that is the case we cannot change these persisted values no matter what value has been posted.
|
||||
// There's only 3 special ones we need to deal with that are part of the MemberSave instance: Comments, IsApproved, IsLockedOut
|
||||
// but we will take care of this in a generic way below so that it works for all props.
|
||||
if (!_backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasAccessToSensitiveData())
|
||||
if (!_backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.HasAccessToSensitiveData())
|
||||
{
|
||||
var memberType = _memberTypeService.Get(contentItem.PersistedContent.ContentTypeId);
|
||||
IMemberType memberType = _memberTypeService.Get(contentItem.PersistedContent.ContentTypeId);
|
||||
var sensitiveProperties = memberType
|
||||
.PropertyTypes.Where(x => memberType.IsSensitiveProperty(x.Alias))
|
||||
.ToList();
|
||||
|
||||
foreach (var sensitiveProperty in sensitiveProperties)
|
||||
foreach (IPropertyType sensitiveProperty in sensitiveProperties)
|
||||
{
|
||||
var destProp = contentItem.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias);
|
||||
ContentPropertyBasic destProp = contentItem.Properties.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias);
|
||||
if (destProp != null)
|
||||
{
|
||||
//if found, change the value of the contentItem model to the persisted value so it remains unchanged
|
||||
var origValue = contentItem.PersistedContent.GetValue(sensitiveProperty.Alias);
|
||||
// if found, change the value of the contentItem model to the persisted value so it remains unchanged
|
||||
object origValue = contentItem.PersistedContent.GetValue(sensitiveProperty.Alias);
|
||||
destProp.Value = origValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isLockedOut = contentItem.IsLockedOut;
|
||||
bool isLockedOut = contentItem.IsLockedOut;
|
||||
|
||||
//if they were locked but now they are trying to be unlocked
|
||||
// if they were locked but now they are trying to be unlocked
|
||||
if (contentItem.PersistedContent.IsLockedOut && isLockedOut == false)
|
||||
{
|
||||
contentItem.PersistedContent.IsLockedOut = false;
|
||||
@@ -371,90 +436,153 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
}
|
||||
else if (!contentItem.PersistedContent.IsLockedOut && isLockedOut)
|
||||
{
|
||||
//NOTE: This should not ever happen unless someone is mucking around with the request data.
|
||||
//An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them
|
||||
// NOTE: This should not ever happen unless someone is mucking around with the request data.
|
||||
// An admin cannot simply lock a user, they get locked out by password attempts, but an admin can un-approve them
|
||||
ModelState.AddModelError("custom", "An admin cannot lock a user");
|
||||
}
|
||||
|
||||
//no password changes then exit ?
|
||||
if (contentItem.Password == null)
|
||||
return;
|
||||
|
||||
throw new NotImplementedException("Members have not been migrated to netcore");
|
||||
// TODO: all member password processing and creation needs to be done with a new aspnet identity MemberUserManager that hasn't been created yet.
|
||||
// set the password
|
||||
//contentItem.PersistedContent.RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword);
|
||||
}
|
||||
|
||||
private static void UpdateName(MemberSave memberSave)
|
||||
{
|
||||
//Don't update the name if it is empty
|
||||
if (memberSave.Name.IsNullOrWhiteSpace() == false)
|
||||
MembersIdentityUser identityMember = await _memberManager.FindByIdAsync(contentItem.Id.ToString());
|
||||
if (identityMember == null)
|
||||
{
|
||||
memberSave.PersistedContent.Name = memberSave.Name;
|
||||
return new ValidationErrorResult("Member was not found");
|
||||
}
|
||||
|
||||
if (contentItem.Password != null)
|
||||
{
|
||||
IdentityResult validatePassword = await _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword);
|
||||
if (validatePassword.Succeeded == false)
|
||||
{
|
||||
return new ValidationErrorResult(validatePassword.Errors.ToErrorMessage());
|
||||
}
|
||||
|
||||
string newPassword = _memberManager.HashPassword(contentItem.Password.NewPassword);
|
||||
identityMember.PasswordHash = newPassword;
|
||||
contentItem.PersistedContent.RawPasswordValue = identityMember.PasswordHash;
|
||||
if (identityMember.LastPasswordChangeDateUtc != null)
|
||||
{
|
||||
contentItem.PersistedContent.LastPasswordChangeDate = DateTime.UtcNow;
|
||||
identityMember.LastPasswordChangeDateUtc = contentItem.PersistedContent.LastPasswordChangeDate;
|
||||
}
|
||||
}
|
||||
|
||||
IdentityResult updatedResult = await _memberManager.UpdateAsync(identityMember);
|
||||
|
||||
if (updatedResult.Succeeded == false)
|
||||
{
|
||||
return new ValidationErrorResult(updatedResult.Errors.ToErrorMessage());
|
||||
}
|
||||
|
||||
_memberService.Save(contentItem.PersistedContent);
|
||||
|
||||
await AddOrUpdateRoles(contentItem, identityMember);
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: This logic should be pulled into the service layer
|
||||
private async Task<bool> ValidateMemberDataAsync(MemberSave contentItem)
|
||||
{
|
||||
if (contentItem.Name.IsNullOrWhiteSpace())
|
||||
{
|
||||
ModelState.AddPropertyError(
|
||||
new ValidationResult("Invalid user name", new[] { "value" }),
|
||||
string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix));
|
||||
new ValidationResult("Invalid user name", new[] { "value" }),
|
||||
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace())
|
||||
{
|
||||
//TODO implement when NETCORE members are implemented
|
||||
throw new NotImplementedException("TODO implement when members are implemented");
|
||||
// var validPassword = await _passwordValidator.ValidateAsync(_passwordConfig, contentItem.Password.NewPassword);
|
||||
// if (!validPassword)
|
||||
// {
|
||||
// ModelState.AddPropertyError(
|
||||
// new ValidationResult("Invalid password: " + string.Join(", ", validPassword.Result), new[] { "value" }),
|
||||
// string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix));
|
||||
// return false;
|
||||
// }
|
||||
IdentityResult validPassword = await _memberManager.ValidatePasswordAsync(contentItem.Password.NewPassword);
|
||||
if (!validPassword.Succeeded)
|
||||
{
|
||||
ModelState.AddPropertyError(
|
||||
new ValidationResult("Invalid password: " + MapErrors(validPassword.Errors), new[] { "value" }),
|
||||
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}password");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var byUsername = _memberService.GetByUsername(contentItem.Username);
|
||||
IMember byUsername = _memberService.GetByUsername(contentItem.Username);
|
||||
if (byUsername != null && byUsername.Key != contentItem.Key)
|
||||
{
|
||||
ModelState.AddPropertyError(
|
||||
new ValidationResult("Username is already in use", new[] { "value" }),
|
||||
string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix));
|
||||
new ValidationResult("Username is already in use", new[] { "value" }),
|
||||
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login");
|
||||
return false;
|
||||
}
|
||||
|
||||
var byEmail = _memberService.GetByEmail(contentItem.Email);
|
||||
IMember byEmail = _memberService.GetByEmail(contentItem.Email);
|
||||
if (byEmail != null && byEmail.Key != contentItem.Key)
|
||||
{
|
||||
ModelState.AddPropertyError(
|
||||
new ValidationResult("Email address is already in use", new[] { "value" }),
|
||||
string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix));
|
||||
new ValidationResult("Email address is already in use", new[] { "value" }),
|
||||
$"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private string MapErrors(IEnumerable<IdentityError> result)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
IEnumerable<IdentityError> identityErrors = result.ToList();
|
||||
foreach (IdentityError error in identityErrors)
|
||||
{
|
||||
string errorString = $"{error.Description}";
|
||||
sb.AppendLine(errorString);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add or update the identity roles
|
||||
/// </summary>
|
||||
/// <param name="contentItem">The member content item</param>
|
||||
/// <param name="identityMember">The member as an identity user</param>
|
||||
private async Task AddOrUpdateRoles(MemberSave contentItem, MembersIdentityUser identityMember)
|
||||
{
|
||||
// We're gonna look up the current roles now because the below code can cause
|
||||
// events to be raised and developers could be manually adding roles to members in
|
||||
// their handlers. If we don't look this up now there's a chance we'll just end up
|
||||
// removing the roles they've assigned.
|
||||
IEnumerable<string> currentRoles = await _memberManager.GetRolesAsync(identityMember);
|
||||
|
||||
// find the ones to remove and remove them
|
||||
IEnumerable<string> roles = currentRoles.ToList();
|
||||
string[] rolesToRemove = roles.Except(contentItem.Groups).ToArray();
|
||||
|
||||
// Now let's do the role provider stuff - now that we've saved the content item (that is important since
|
||||
// if we are changing the username, it must be persisted before looking up the member roles).
|
||||
if (rolesToRemove.Any())
|
||||
{
|
||||
IdentityResult rolesIdentityResult = await _memberManager.RemoveFromRolesAsync(identityMember, rolesToRemove);
|
||||
}
|
||||
|
||||
// find the ones to add and add them
|
||||
string[] toAdd = contentItem.Groups.Except(roles).ToArray();
|
||||
if (toAdd.Any())
|
||||
{
|
||||
// add the ones submitted
|
||||
IdentityResult identityResult = await _memberManager.AddToRolesAsync(identityMember, toAdd);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Permanently deletes a member
|
||||
/// </summary>
|
||||
/// <param name="key"></param>
|
||||
/// <returns></returns>
|
||||
/// <param name="key">Guid of the member to delete</param>
|
||||
/// <returns>The result of the deletion</returns>
|
||||
///
|
||||
[HttpPost]
|
||||
public IActionResult DeleteByKey(Guid key)
|
||||
{
|
||||
var foundMember = _memberService.GetByKey(key);
|
||||
//TODO: move to MembersUserStore
|
||||
IMember foundMember = _memberService.GetByKey(key);
|
||||
if (foundMember == null)
|
||||
{
|
||||
return HandleContentNotFound(key);
|
||||
}
|
||||
|
||||
_memberService.Delete(foundMember);
|
||||
|
||||
return Ok();
|
||||
@@ -468,25 +596,27 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
|
||||
[HttpGet]
|
||||
public IActionResult ExportMemberData(Guid key)
|
||||
{
|
||||
var currentUser = _backofficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
|
||||
IUser currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser;
|
||||
|
||||
if (currentUser.HasAccessToSensitiveData() == false)
|
||||
{
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
var member = ((MemberService)_memberService).ExportMember(key);
|
||||
if (member is null) throw new NullReferenceException("No member found with key " + key);
|
||||
MemberExportModel member = ((MemberService)_memberService).ExportMember(key);
|
||||
if (member is null)
|
||||
{
|
||||
throw new NullReferenceException("No member found with key " + key);
|
||||
}
|
||||
|
||||
var json = _jsonSerializer.Serialize(member);
|
||||
|
||||
var fileName = $"{member.Name}_{member.Email}.txt";
|
||||
|
||||
// Set custom header so umbRequestHelper.downloadFile can save the correct filename
|
||||
HttpContext.Response.Headers.Add("x-filename", fileName);
|
||||
|
||||
return File( Encoding.UTF8.GetBytes(json), MediaTypeNames.Application.Octet, fileName);
|
||||
return File(Encoding.UTF8.GetBytes(json), MediaTypeNames.Application.Octet, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user