From 558c0b03ee89db941728676537ec2ba84069864a Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 18 Jun 2020 21:03:11 +0200 Subject: [PATCH] Migrated MemberController Signed-off-by: Bjarke Berg --- .../Controllers/BackOfficeServerVariables.cs | 20 +- .../Controllers/MemberController.cs | 958 +++++++++--------- .../Filters/MemberSaveModelValidator.cs | 195 ++++ .../Filters/MemberSaveValidationAttribute.cs | 57 ++ .../Trees/MemberTypeTreeController.cs | 2 + .../OutgoingNoHyphenGuidFormatAttribute.cs | 86 ++ .../Filters/MemberSaveValidationAttribute.cs | 49 - src/Umbraco.Web/Umbraco.Web.csproj | 3 - .../OutgoingNoHyphenGuidFormatAttribute.cs | 24 - .../WebApi/GuidNoHyphenConverter.cs | 40 - 10 files changed, 832 insertions(+), 602 deletions(-) create mode 100644 src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs create mode 100644 src/Umbraco.Web.BackOffice/Filters/MemberSaveValidationAttribute.cs create mode 100644 src/Umbraco.Web.Common/Filters/OutgoingNoHyphenGuidFormatAttribute.cs delete mode 100644 src/Umbraco.Web/Editors/Filters/MemberSaveValidationAttribute.cs delete mode 100644 src/Umbraco.Web/WebApi/Filters/OutgoingNoHyphenGuidFormatAttribute.cs delete mode 100644 src/Umbraco.Web/WebApi/GuidNoHyphenConverter.cs diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 3b72ca4d2d..1355541bb8 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -143,8 +143,8 @@ namespace Umbraco.Web.BackOffice.Controllers // having each url defined here explicitly - we can do that in v8! for now // for umbraco services we'll stick to explicitly defining the endpoints. - //{"externalLoginsUrl", _linkGenerator.GetPathByAction(nameof(BackOfficeController.ExternalLogin), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })}, - //{"externalLinkLoginsUrl", _linkGenerator.GetPathByAction(nameof(BackOfficeController.LinkLogin), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })}, + // {"externalLoginsUrl", _linkGenerator.GetPathByAction(nameof(BackOfficeController.ExternalLogin), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })}, + // {"externalLinkLoginsUrl", _linkGenerator.GetPathByAction(nameof(BackOfficeController.LinkLogin), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })}, {"gridConfig", _linkGenerator.GetPathByAction(nameof(BackOfficeController.GetGridConfig), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })}, // TODO: This is ultra confusing! this same key is used for different things, when returning the full app when authenticated it is this URL but when not auth'd it's actually the ServerVariables address {"serverVarsJs", _linkGenerator.GetPathByAction(nameof(BackOfficeController.Application), backOfficeControllerName, new { area = Constants.Web.Mvc.BackOfficeArea })}, @@ -232,10 +232,10 @@ namespace Umbraco.Web.BackOffice.Controllers "logApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.GetPagedEntityLog(0, 0, 0, Direction.Ascending, null)) }, - // { - // "memberApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( - // controller => controller.GetByKey(Guid.Empty)) - // }, + { + "memberApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( + controller => controller.GetByKey(Guid.Empty)) + }, { "packageInstallApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.Fetch(string.Empty)) @@ -256,10 +256,10 @@ namespace Umbraco.Web.BackOffice.Controllers "stylesheetApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.GetAll()) }, - // { - // "memberTypeApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( - // controller => controller.GetAllTypes()) - // }, + { + "memberTypeApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( + controller => controller.GetAllTypes()) + }, // { // "memberGroupApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( // controller => controller.GetAllGroups()) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs index 5048a3251f..0680041c42 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberController.cs @@ -1,476 +1,482 @@ -// using System; -// using System.Collections.Generic; -// using System.ComponentModel.DataAnnotations; -// using System.Linq; -// using System.Net; -// using System.Net.Http; -// using System.Net.Http.Formatting; -// using System.Net.Http.Headers; -// using System.Threading.Tasks; -// using System.Web.Http; -// using System.Web.Http.ModelBinding; -// using Microsoft.AspNetCore.Identity; -// using Microsoft.AspNetCore.Mvc; -// using Umbraco.Core; -// using Umbraco.Core.Cache; -// using Umbraco.Core.Configuration; -// using Umbraco.Core.Dictionary; -// using Umbraco.Core.Logging; -// using Umbraco.Core.Models; -// using Umbraco.Core.Models.ContentEditing; -// using Umbraco.Core.Models.Membership; -// using Umbraco.Core.Persistence; -// using Umbraco.Core.PropertyEditors; -// using Umbraco.Core.Security; -// using Umbraco.Core.Services; -// using Umbraco.Core.Services.Implement; -// using Umbraco.Core.Strings; -// using Umbraco.Web.ContentApps; -// using Umbraco.Web.Editors.Binders; -// using Umbraco.Web.Editors.Filters; -// using Umbraco.Web.Models.ContentEditing; -// using Umbraco.Web.Mvc; -// using Umbraco.Web.WebApi; -// using Umbraco.Web.WebApi.Filters; -// using Constants = Umbraco.Core.Constants; -// using Umbraco.Core.Mapping; -// using Umbraco.Extensions; -// using Umbraco.Web.BackOffice.Filters; -// using Umbraco.Web.Common.Attributes; -// using Umbraco.Web.Common.Exceptions; -// using Umbraco.Web.Routing; -// -// namespace Umbraco.Web.Editors -// { -// /// -// /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting -// /// access to ALL of the methods on this controller will need access to the member application. -// /// -// [PluginController("UmbracoApi")] -// [UmbracoApplicationAuthorize(Constants.Applications.Members)] -// [OutgoingNoHyphenGuidFormat] -// public class MemberController : ContentControllerBase -// { -// public MemberController( -// IMemberPasswordConfiguration passwordConfig, -// ICultureDictionary cultureDictionary, -// PropertyEditorCollection propertyEditors, -// IGlobalSettings globalSettings, -// IUmbracoContextAccessor umbracoContextAccessor, -// ISqlContext sqlContext, -// ServiceContext services, -// AppCaches appCaches, -// IProfilingLogger logger, -// IRuntimeState runtimeState, -// IShortStringHelper shortStringHelper, -// UmbracoMapper umbracoMapper, -// IPublishedUrlProvider publishedUrlProvider) -// : base(cultureDictionary, globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider) -// { -// _passwordConfig = passwordConfig ?? throw new ArgumentNullException(nameof(passwordConfig)); -// _propertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors)); -// _passwordSecurity = new LegacyPasswordSecurity(_passwordConfig); -// _passwordValidator = new ConfiguredPasswordValidator(); -// } -// -// private readonly IMemberPasswordConfiguration _passwordConfig; -// private readonly PropertyEditorCollection _propertyEditors; -// private readonly LegacyPasswordSecurity _passwordSecurity; -// private readonly IPasswordValidator<> _passwordValidator; -// -// public PagedResult GetPagedResults( -// int pageNumber = 1, -// int pageSize = 100, -// string orderBy = "username", -// Direction orderDirection = Direction.Ascending, -// bool orderBySystemField = true, -// string filter = "", -// string memberTypeAlias = null) -// { -// -// if (pageNumber <= 0 || pageSize <= 0) -// { -// throw new NotSupportedException("Both pageNumber and pageSize must be greater than zero"); -// } -// -// var members = Services.MemberService -// .GetAll((pageNumber - 1), pageSize, out var totalRecords, orderBy, orderDirection, orderBySystemField, memberTypeAlias, filter).ToArray(); -// if (totalRecords == 0) -// { -// return new PagedResult(0, 0, 0); -// } -// -// var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) -// { -// Items = members -// .Select(x => Mapper.Map(x)) -// }; -// return pagedResult; -// } -// -// /// -// /// Returns a display node with a list view to render members -// /// -// /// -// /// -// public MemberListDisplay GetListNodeDisplay(string listName) -// { -// var foundType = Services.MemberTypeService.Get(listName); -// var name = foundType != null ? foundType.Name : listName; -// -// var apps = new List(); -// apps.Add(ListViewContentAppFactory.CreateContentApp(Services.DataTypeService, _propertyEditors, listName, "member", Core.Constants.DataTypes.DefaultMembersListView)); -// apps[0].Active = true; -// -// var display = new MemberListDisplay -// { -// ContentTypeAlias = listName, -// ContentTypeName = name, -// Id = listName, -// IsContainer = true, -// Name = listName == Constants.Conventions.MemberTypes.AllMembersListId ? "All Members" : name, -// Path = "-1," + listName, -// ParentId = -1, -// ContentApps = apps -// }; -// -// return display; -// } -// -// /// -// /// Gets the content json for the member -// /// -// /// -// /// -// // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core -// public MemberDisplay GetByKey(Guid key) -// { -// var foundMember = Services.MemberService.GetByKey(key); -// if (foundMember == null) -// { -// HandleContentNotFound(key); -// } -// return Mapper.Map(foundMember); -// } -// -// /// -// /// Gets an empty content item for the -// /// -// /// -// /// -// // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core -// public MemberDisplay GetEmpty(string contentTypeAlias = null) -// { -// IMember emptyContent; -// if (contentTypeAlias == null) -// { -// throw new HttpResponseException(HttpStatusCode.NotFound); -// } -// -// var contentType = Services.MemberTypeService.Get(contentTypeAlias); -// if (contentType == null) -// { -// throw new HttpResponseException(HttpStatusCode.NotFound); -// } -// -// var passwordGenerator = new PasswordGenerator(_passwordConfig); -// -// emptyContent = new Member(contentType); -// emptyContent.AdditionalData["NewPassword"] = passwordGenerator.GeneratePassword(); -// return Mapper.Map(emptyContent); -// } -// -// /// -// /// Saves member -// /// -// /// -// [FileUploadCleanupFilter] -// // [OutgoingEditorModelEvent] // TODO introduce when moved to .NET Core -// [MemberSaveValidation] -// public async Task PostSave( -// [ModelBinder(typeof(MemberBinder))] -// MemberSave contentItem) -// { -// -// //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 -// 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 -// if (ModelState.IsValid == false) -// { -// var forDisplay = Mapper.Map(contentItem.PersistedContent); -// forDisplay.Errors = ModelState.ToErrorDictionary(); -// throw new HttpResponseException(Request.CreateValidationErrorResponse(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 = Services.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 -// // this ensures that passwords are formatted correctly and also performs the validation on the provider itself. -// switch (contentItem.Action) -// { -// case ContentSaveAction.Save: -// UpdateMemberData(contentItem); -// break; -// case ContentSaveAction.SaveNew: -// contentItem.PersistedContent = CreateMemberData(contentItem); -// break; -// default: -// //we don't support anything else for members -// throw new HttpResponseException(HttpStatusCode.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 -// // but it would be nicer to have this taken care of within the Save method itself -// -// //create/save the IMember -// Services.MemberService.Save(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()) -// { -// Services.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 -// Services.MemberService.AssignRoles(new[] { contentItem.PersistedContent.Username }, toAdd); -// } -// -// //return the updated model -// var display = Mapper.Map(contentItem.PersistedContent); -// -// //lastly, if it is not valid, add the model state to the outgoing object and throw a 403 -// HandleInvalidModelState(display); -// -// var localizedTextService = Services.TextService; -// //put the correct messages in -// switch (contentItem.Action) -// { -// case ContentSaveAction.Save: -// case ContentSaveAction.SaveNew: -// display.AddSuccessNotification(localizedTextService.Localize("speechBubbles/editMemberSaved"), localizedTextService.Localize("speechBubbles/editMemberSaved")); -// break; -// } -// -// return display; -// } -// -// /// -// /// Maps the property values to the persisted entity -// /// -// /// -// private void MapPropertyValues(MemberSave contentItem) -// { -// UpdateName(contentItem); -// -// //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( -// contentItem, -// contentItem.PropertyCollectionDto, -// (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) -// { -// var memberType = Services.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 = Security.CurrentUser.Id, -// RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword), -// Comments = contentItem.Comments, -// IsApproved = contentItem.IsApproved -// }; -// -// return member; -// } -// -// /// -// /// Update the member security data -// /// -// /// -// /// -// /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. -// /// -// private void UpdateMemberData(MemberSave contentItem) -// { -// contentItem.PersistedContent.WriterId = Security.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 (!Security.CurrentUser.HasAccessToSensitiveData()) -// { -// var memberType = Services.MemberTypeService.Get(contentItem.PersistedContent.ContentTypeId); -// var sensitiveProperties = memberType -// .PropertyTypes.Where(x => memberType.IsSensitiveProperty(x.Alias)) -// .ToList(); -// -// foreach (var sensitiveProperty in sensitiveProperties) -// { -// var 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); -// destProp.Value = origValue; -// } -// } -// } -// -// var isLockedOut = contentItem.IsLockedOut; -// -// //if they were locked but now they are trying to be unlocked -// if (contentItem.PersistedContent.IsLockedOut && isLockedOut == false) -// { -// contentItem.PersistedContent.IsLockedOut = false; -// contentItem.PersistedContent.FailedPasswordAttempts = 0; -// } -// 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 -// ModelState.AddModelError("custom", "An admin cannot lock a user"); -// } -// -// //no password changes then exit ? -// if (contentItem.Password == null) -// return; -// -// // 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) -// { -// memberSave.PersistedContent.Name = memberSave.Name; -// } -// } -// -// // TODO: This logic should be pulled into the service layer -// private async Task ValidateMemberDataAsync(MemberSave contentItem) -// { -// if (contentItem.Name.IsNullOrWhiteSpace()) -// { -// ModelState.AddPropertyError( -// new ValidationResult("Invalid user name", new[] { "value" }), -// string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); -// return false; -// } -// -// if (contentItem.Password != null && !contentItem.Password.NewPassword.IsNullOrWhiteSpace()) -// { -// 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; -// } -// } -// -// var byUsername = Services.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)); -// return false; -// } -// -// var byEmail = Services.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)); -// return false; -// } -// -// return true; -// } -// -// /// -// /// Permanently deletes a member -// /// -// /// -// /// -// /// -// [HttpPost] -// public HttpResponseMessage DeleteByKey(Guid key) -// { -// var foundMember = Services.MemberService.GetByKey(key); -// if (foundMember == null) -// { -// return HandleContentNotFound(key, false); -// } -// Services.MemberService.Delete(foundMember); -// -// return Request.CreateResponse(HttpStatusCode.OK); -// } -// -// /// -// /// Exports member data based on their unique Id -// /// -// /// The unique member identifier -// /// -// [HttpGet] -// public HttpResponseMessage ExportMemberData(Guid key) -// { -// var currentUser = Security.CurrentUser; -// -// var httpResponseMessage = Request.CreateResponse(); -// if (currentUser.HasAccessToSensitiveData() == false) -// { -// httpResponseMessage.StatusCode = HttpStatusCode.Forbidden; -// return httpResponseMessage; -// } -// -// var member = ((MemberService)Services.MemberService).ExportMember(key); -// -// var fileName = $"{member.Name}_{member.Email}.txt"; -// -// httpResponseMessage.Content = new ObjectContent(member, new JsonMediaTypeFormatter { Indent = true }); -// httpResponseMessage.Content.Headers.Add("x-filename", fileName); -// httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); -// httpResponseMessage.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment"); -// httpResponseMessage.Content.Headers.ContentDisposition.FileName = fileName; -// httpResponseMessage.StatusCode = HttpStatusCode.OK; -// -// return httpResponseMessage; -// } -// } -// -// -// } +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Net.Mime; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Core; +using Umbraco.Core.Configuration; +using Umbraco.Core.Dictionary; +using Umbraco.Core.Events; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.ContentEditing; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Security; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; +using Umbraco.Web.ContentApps; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.WebApi.Filters; +using Constants = Umbraco.Core.Constants; +using Umbraco.Core.Mapping; +using Umbraco.Core.Serialization; +using Umbraco.Core.Strings; +using Umbraco.Extensions; +using Umbraco.Web.BackOffice.Filters; +using Umbraco.Web.BackOffice.ModelBinders; +using Umbraco.Web.Common.Attributes; +using Umbraco.Web.Common.Exceptions; +using Umbraco.Web.Common.Filters; +using Umbraco.Web.Security; + +namespace Umbraco.Web.BackOffice.Controllers +{ + /// + /// This controller is decorated with the UmbracoApplicationAuthorizeAttribute which means that any user requesting + /// access to ALL of the methods on this controller will need access to the member application. + /// + [PluginController("UmbracoApi")] + [UmbracoApplicationAuthorize(Constants.Applications.Members)] + [OutgoingNoHyphenGuidFormat] + public class MemberController : ContentControllerBase + { + + private readonly IMemberPasswordConfiguration _passwordConfig; + private readonly PropertyEditorCollection _propertyEditors; + private readonly LegacyPasswordSecurity _passwordSecurity; + private readonly UmbracoMapper _umbracoMapper; + private readonly IMemberService _memberService; + private readonly IMemberTypeService _memberTypeService; + private readonly IDataTypeService _dataTypeService; + private readonly ILocalizedTextService _localizedTextService; + private readonly IWebSecurity _webSecurity; + private readonly IJsonSerializer _jsonSerializer; + + public MemberController( + ICultureDictionary cultureDictionary, + ILogger logger, + IShortStringHelper shortStringHelper, + IEventMessagesFactory eventMessages, + ILocalizedTextService localizedTextService, + IMemberPasswordConfiguration passwordConfig, + PropertyEditorCollection propertyEditors, + LegacyPasswordSecurity passwordSecurity, + UmbracoMapper umbracoMapper, + IMemberService memberService, + IMemberTypeService memberTypeService, + IDataTypeService dataTypeService, + IWebSecurity webSecurity, + IJsonSerializer jsonSerializer) + : base(cultureDictionary, logger, shortStringHelper, eventMessages, localizedTextService) + { + _passwordConfig = passwordConfig; + _propertyEditors = propertyEditors; + _passwordSecurity = passwordSecurity; + _umbracoMapper = umbracoMapper; + _memberService = memberService; + _memberTypeService = memberTypeService; + _dataTypeService = dataTypeService; + _localizedTextService = localizedTextService; + _webSecurity = webSecurity; + _jsonSerializer = jsonSerializer; + } + + public PagedResult GetPagedResults( + int pageNumber = 1, + int pageSize = 100, + string orderBy = "username", + Direction orderDirection = Direction.Ascending, + bool orderBySystemField = true, + string filter = "", + string memberTypeAlias = null) + { + + if (pageNumber <= 0 || pageSize <= 0) + { + 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(); + if (totalRecords == 0) + { + return new PagedResult(0, 0, 0); + } + + var pagedResult = new PagedResult(totalRecords, pageNumber, pageSize) + { + Items = members + .Select(x => _umbracoMapper.Map(x)) + }; + return pagedResult; + } + + /// + /// Returns a display node with a list view to render members + /// + /// + /// + public MemberListDisplay GetListNodeDisplay(string listName) + { + var foundType = _memberTypeService.Get(listName); + var name = foundType != null ? foundType.Name : listName; + + var apps = new List(); + apps.Add(ListViewContentAppFactory.CreateContentApp(_dataTypeService, _propertyEditors, listName, "member", Core.Constants.DataTypes.DefaultMembersListView)); + apps[0].Active = true; + + var display = new MemberListDisplay + { + ContentTypeAlias = listName, + ContentTypeName = name, + Id = listName, + IsContainer = true, + Name = listName == Constants.Conventions.MemberTypes.AllMembersListId ? "All Members" : name, + Path = "-1," + listName, + ParentId = -1, + ContentApps = apps + }; + + return display; + } + + /// + /// Gets the content json for the member + /// + /// + /// + [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] + public MemberDisplay GetByKey(Guid key) + { + var foundMember = _memberService.GetByKey(key); + if (foundMember == null) + { + HandleContentNotFound(key); + } + return _umbracoMapper.Map(foundMember); + } + + /// + /// Gets an empty content item for the + /// + /// + /// + [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] + public MemberDisplay GetEmpty(string contentTypeAlias = null) + { + IMember emptyContent; + if (contentTypeAlias == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var contentType = _memberTypeService.Get(contentTypeAlias); + if (contentType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var passwordGenerator = new PasswordGenerator(_passwordConfig); + + emptyContent = new Member(contentType); + emptyContent.AdditionalData["NewPassword"] = passwordGenerator.GeneratePassword(); + return _umbracoMapper.Map(emptyContent); + } + + /// + /// Saves member + /// + /// + [FileUploadCleanupFilter] + [TypeFilter(typeof(OutgoingEditorModelEventAttribute))] + [MemberSaveValidation] + public async Task> PostSave( + [ModelBinder(typeof(MemberBinder))] + MemberSave contentItem) + { + + //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 + 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 + if (ModelState.IsValid == false) + { + var forDisplay = _umbracoMapper.Map(contentItem.PersistedContent); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw HttpResponseException.CreateValidationErrorResponse(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 + // this ensures that passwords are formatted correctly and also performs the validation on the provider itself. + switch (contentItem.Action) + { + case ContentSaveAction.Save: + UpdateMemberData(contentItem); + break; + case ContentSaveAction.SaveNew: + contentItem.PersistedContent = CreateMemberData(contentItem); + break; + default: + //we don't support anything else for members + throw new HttpResponseException(HttpStatusCode.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 + // but it would be nicer to have this taken care of within the Save method itself + + //create/save the IMember + _memberService.Save(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(contentItem.PersistedContent); + + //lastly, if it is not valid, add the model state to the outgoing object and throw a 403 + HandleInvalidModelState(display); + + var 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")); + break; + } + + return display; + } + + /// + /// Maps the property values to the persisted entity + /// + /// + private void MapPropertyValues(MemberSave contentItem) + { + UpdateName(contentItem); + + //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( + contentItem, + contentItem.PropertyCollectionDto, + (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) + { + 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 = _webSecurity.CurrentUser.Id, + RawPasswordValue = _passwordSecurity.HashPasswordForStorage(contentItem.Password.NewPassword), + Comments = contentItem.Comments, + IsApproved = contentItem.IsApproved + }; + + return member; + } + + /// + /// Update the member security data + /// + /// + /// + /// If the password has been reset then this method will return the reset/generated password, otherwise will return null. + /// + private void UpdateMemberData(MemberSave contentItem) + { + contentItem.PersistedContent.WriterId = _webSecurity.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 (!_webSecurity.CurrentUser.HasAccessToSensitiveData()) + { + var memberType = _memberTypeService.Get(contentItem.PersistedContent.ContentTypeId); + var sensitiveProperties = memberType + .PropertyTypes.Where(x => memberType.IsSensitiveProperty(x.Alias)) + .ToList(); + + foreach (var sensitiveProperty in sensitiveProperties) + { + var 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); + destProp.Value = origValue; + } + } + } + + var isLockedOut = contentItem.IsLockedOut; + + //if they were locked but now they are trying to be unlocked + if (contentItem.PersistedContent.IsLockedOut && isLockedOut == false) + { + contentItem.PersistedContent.IsLockedOut = false; + contentItem.PersistedContent.FailedPasswordAttempts = 0; + } + 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 + ModelState.AddModelError("custom", "An admin cannot lock a user"); + } + + //no password changes then exit ? + if (contentItem.Password == null) + return; + + // 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) + { + memberSave.PersistedContent.Name = memberSave.Name; + } + } + + // TODO: This logic should be pulled into the service layer + private async Task ValidateMemberDataAsync(MemberSave contentItem) + { + if (contentItem.Name.IsNullOrWhiteSpace()) + { + ModelState.AddPropertyError( + new ValidationResult("Invalid user name", new[] { "value" }), + string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + 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; + // } + } + + var 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)); + return false; + } + + var 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)); + return false; + } + + return true; + } + + /// + /// Permanently deletes a member + /// + /// + /// + /// + [HttpPost] + public IActionResult DeleteByKey(Guid key) + { + var foundMember = _memberService.GetByKey(key); + if (foundMember == null) + { + return HandleContentNotFound(key, false); + } + _memberService.Delete(foundMember); + + return Ok(); + } + + /// + /// Exports member data based on their unique Id + /// + /// The unique member identifier + /// + [HttpGet] + public IActionResult ExportMemberData(Guid key) + { + var currentUser = _webSecurity.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); + + 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); + } + } + + +} diff --git a/src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs b/src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs new file mode 100644 index 0000000000..f2cf72b41c --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Filters/MemberSaveModelValidator.cs @@ -0,0 +1,195 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.Extensions; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Security; + +namespace Umbraco.Web.BackOffice.Filters +{ + /// + /// Validator for + /// + internal class MemberSaveModelValidator : ContentModelValidator> + { + private readonly IMemberTypeService _memberTypeService; + private readonly IMemberService _memberService; + private readonly IShortStringHelper _shortStringHelper; + + public MemberSaveModelValidator( + ILogger logger, + IWebSecurity webSecurity, + ILocalizedTextService textService, + IMemberTypeService memberTypeService, + IMemberService memberService, + IShortStringHelper shortStringHelper) + : base(logger, webSecurity, textService) + { + _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); + _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); + _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); + } + + public override bool ValidatePropertiesData(MemberSave model, IContentProperties modelWithProperties, ContentPropertyCollectionDto dto, + ModelStateDictionary modelState) + { + if (model.Username.IsNullOrWhiteSpace()) + { + modelState.AddPropertyError( + new ValidationResult("Invalid user name", new[] { "value" }), + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login"); + } + + if (model.Email.IsNullOrWhiteSpace() || new EmailAddressAttribute().IsValid(model.Email) == false) + { + modelState.AddPropertyError( + new ValidationResult("Invalid email", new[] { "value" }), + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email"); + } + + var validEmail = ValidateUniqueEmail(model); + if (validEmail == false) + { + modelState.AddPropertyError( + new ValidationResult("Email address is already in use", new[] { "value" }), + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}email"); + } + + var validLogin = ValidateUniqueLogin(model); + if (validLogin == false) + { + modelState.AddPropertyError( + new ValidationResult("Username is already in use", new[] { "value" }), + $"{Constants.PropertyEditors.InternalGenericPropertiesPrefix}login"); + } + + return base.ValidatePropertiesData(model, modelWithProperties, dto, modelState); + } + + /// + /// This ensures that the internal membership property types are removed from validation before processing the validation + /// since those properties are actually mapped to real properties of the IMember. + /// This also validates any posted data for fields that are sensitive. + /// + /// + /// + /// + /// + public override bool ValidateProperties(MemberSave model, IContentProperties modelWithProperties, ActionExecutingContext actionContext) + { + var propertiesToValidate = model.Properties.ToList(); + var defaultProps = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper); + var exclude = defaultProps.Select(x => x.Value.Alias).ToArray(); + foreach (var remove in exclude) + { + propertiesToValidate.RemoveAll(property => property.Alias == remove); + } + + //if the user doesn't have access to sensitive values, then we need to validate the incoming properties to check + //if a sensitive value is being submitted. + if (WebSecurity.CurrentUser.HasAccessToSensitiveData() == false) + { + var contentType = _memberTypeService.Get(model.PersistedContent.ContentTypeId); + var sensitiveProperties = contentType + .PropertyTypes.Where(x => contentType.IsSensitiveProperty(x.Alias)) + .ToList(); + + foreach (var sensitiveProperty in sensitiveProperties) + { + var prop = propertiesToValidate.FirstOrDefault(x => x.Alias == sensitiveProperty.Alias); + + if (prop != null) + { + //this should not happen, this means that there was data posted for a sensitive property that + //the user doesn't have access to, which means that someone is trying to hack the values. + + var message = $"property with alias: {prop.Alias} cannot be posted"; + actionContext.Result = new NotFoundObjectResult(new InvalidOperationException(message)); + return false; + } + } + } + + return ValidateProperties(propertiesToValidate, model.PersistedContent.Properties.ToList(), actionContext); + } + + internal bool ValidateUniqueLogin(MemberSave model) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + + var existingByName = _memberService.GetByUsername(model.Username.Trim()); + switch (model.Action) + { + case ContentSaveAction.Save: + + //ok, we're updating the member, we need to check if they are changing their login and if so, does it exist already ? + if (model.PersistedContent.Username.InvariantEquals(model.Username.Trim()) == false) + { + //they are changing their login name + if (existingByName != null && existingByName.Username == model.Username.Trim()) + { + //the user cannot use this login + return false; + } + } + break; + case ContentSaveAction.SaveNew: + //check if the user's login already exists + if (existingByName != null && existingByName.Username == model.Username.Trim()) + { + //the user cannot use this login + return false; + } + break; + default: + //we don't support this for members + throw new ArgumentOutOfRangeException(); + } + + return true; + } + + internal bool ValidateUniqueEmail(MemberSave model) + { + if (model == null) throw new ArgumentNullException(nameof(model)); + + var existingByEmail = _memberService.GetByEmail(model.Email.Trim()); + switch (model.Action) + { + case ContentSaveAction.Save: + //ok, we're updating the member, we need to check if they are changing their email and if so, does it exist already ? + if (model.PersistedContent.Email.InvariantEquals(model.Email.Trim()) == false) + { + //they are changing their email + if (existingByEmail != null && existingByEmail.Email.InvariantEquals(model.Email.Trim())) + { + //the user cannot use this email + return false; + } + } + break; + case ContentSaveAction.SaveNew: + //check if the user's email already exists + if (existingByEmail != null && existingByEmail.Email.InvariantEquals(model.Email.Trim())) + { + //the user cannot use this email + return false; + } + break; + default: + //we don't support this for members + throw new ArgumentOutOfRangeException(); + } + + return true; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Filters/MemberSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/MemberSaveValidationAttribute.cs new file mode 100644 index 0000000000..8b1e9d38b2 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Filters/MemberSaveValidationAttribute.cs @@ -0,0 +1,57 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Umbraco.Core.Logging; +using Umbraco.Core.Services; +using Umbraco.Core.Strings; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Security; + +namespace Umbraco.Web.BackOffice.Filters +{ + /// + /// Validates the incoming model + /// + internal sealed class MemberSaveValidationAttribute : TypeFilterAttribute + { + public MemberSaveValidationAttribute() : base(typeof(MemberSaveValidationFilter)) + { + + } + + private sealed class MemberSaveValidationFilter : IActionFilter + { + private readonly ILogger _logger; + private readonly IWebSecurity _webSecurity; + private readonly ILocalizedTextService _textService; + private readonly IMemberTypeService _memberTypeService; + private readonly IMemberService _memberService; + private readonly IShortStringHelper _shortStringHelper; + + public MemberSaveValidationFilter(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService, IMemberTypeService memberTypeService, IMemberService memberService, IShortStringHelper shortStringHelper) + { + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); + _textService = textService ?? throw new ArgumentNullException(nameof(textService)); + _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); + _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); + _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); + } + + public void OnActionExecuting(ActionExecutingContext context) + { + var model = (MemberSave)context.ActionArguments["contentItem"]; + var contentItemValidator = new MemberSaveModelValidator(_logger, _webSecurity, _textService, _memberTypeService, _memberService, _shortStringHelper); + //now do each validation step + if (contentItemValidator.ValidateExistingContent(model, context)) + if (contentItemValidator.ValidateProperties(model, model, context)) + contentItemValidator.ValidatePropertiesData(model, model, model.PropertyCollectionDto, context.ModelState); + } + + public void OnActionExecuted(ActionExecutedContext context) + { + } + + } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs index 169689897a..b2d4a172fb 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs @@ -6,6 +6,7 @@ using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Web.BackOffice.Filters; using Umbraco.Web.BackOffice.Trees; +using Umbraco.Web.Common.Attributes; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Models.Trees; using Umbraco.Web.Search; @@ -16,6 +17,7 @@ namespace Umbraco.Web.Trees [CoreTree] [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] [Tree(Constants.Applications.Settings, Constants.Trees.MemberTypes, SortOrder = 2, TreeGroup = Constants.Trees.Groups.Settings)] + [PluginController("UmbracoTrees")] public class MemberTypeTreeController : MemberTypeAndGroupTreeControllerBase, ISearchableTree { private readonly UmbracoTreeSearcher _treeSearcher; diff --git a/src/Umbraco.Web.Common/Filters/OutgoingNoHyphenGuidFormatAttribute.cs b/src/Umbraco.Web.Common/Filters/OutgoingNoHyphenGuidFormatAttribute.cs new file mode 100644 index 0000000000..02fbbce9e2 --- /dev/null +++ b/src/Umbraco.Web.Common/Filters/OutgoingNoHyphenGuidFormatAttribute.cs @@ -0,0 +1,86 @@ +using System; +using System.Buffers; +using System.Linq; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.Mvc.Formatters; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Umbraco.Core; +using Umbraco.Web.Common.Formatters; + +namespace Umbraco.Web.Common.Filters +{ + public class OutgoingNoHyphenGuidFormatAttribute : TypeFilterAttribute + { + public OutgoingNoHyphenGuidFormatAttribute() : base(typeof(OutgoingNoHyphenGuidFormatFilter)) + { + Order = -2000; + } + + private class OutgoingNoHyphenGuidFormatFilter : IResultFilter + { + private readonly IOptions _mvcNewtonsoftJsonOptions; + private readonly ArrayPool _arrayPool; + private readonly IOptions _options; + + public OutgoingNoHyphenGuidFormatFilter(ArrayPool arrayPool, IOptions options) + { + _arrayPool = arrayPool; + _options = options; + } + public void OnResultExecuted(ResultExecutedContext context) + { + } + + public void OnResultExecuting(ResultExecutingContext context) + { + if (context.Result is ObjectResult objectResult) + { + var serializerSettings = new JsonSerializerSettings(); + serializerSettings.Converters.Add(new GuidNoHyphenConverter()); + + objectResult.Formatters.Clear(); + objectResult.Formatters.Add(new AngularJsonMediaTypeFormatter(serializerSettings, _arrayPool, _options.Value)); + } + } + + /// + /// A custom converter for GUID's to format without hyphens + /// + private class GuidNoHyphenConverter : JsonConverter + { + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + switch (reader.TokenType) + { + case JsonToken.Null: + return Guid.Empty; + case JsonToken.String: + var guidAttempt = reader.Value.TryConvertTo(); + if (guidAttempt.Success) + { + return guidAttempt.Result; + } + throw new FormatException("Could not convert " + reader.Value + " to a GUID"); + default: + throw new ArgumentException("Invalid token type"); + } + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + writer.WriteValue(Guid.Empty.Equals(value) ? Guid.Empty.ToString("N") : ((Guid)value).ToString("N")); + } + + public override bool CanConvert(Type objectType) + { + return typeof(Guid) == objectType; + } + } + } + } + + +} diff --git a/src/Umbraco.Web/Editors/Filters/MemberSaveValidationAttribute.cs b/src/Umbraco.Web/Editors/Filters/MemberSaveValidationAttribute.cs deleted file mode 100644 index a3739c1002..0000000000 --- a/src/Umbraco.Web/Editors/Filters/MemberSaveValidationAttribute.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; -using Umbraco.Core.Logging; -using Umbraco.Core.Services; -using Umbraco.Core.Strings; -using Umbraco.Web.Composing; -using Umbraco.Web.Models.ContentEditing; -using Umbraco.Web.Security; - -namespace Umbraco.Web.Editors.Filters -{ - /// - /// Validates the incoming model - /// - internal class MemberSaveValidationAttribute : ActionFilterAttribute - { - private readonly ILogger _logger; - private readonly IWebSecurity _webSecurity; - private readonly ILocalizedTextService _textService; - private readonly IMemberTypeService _memberTypeService; - private readonly IMemberService _memberService; - private readonly IShortStringHelper _shortStringHelper; - - public MemberSaveValidationAttribute() - : this(Current.Logger, Current.UmbracoContextAccessor.UmbracoContext.Security, Current.Services.TextService, Current.Services.MemberTypeService, Current.Services.MemberService, Current.ShortStringHelper) - { } - - public MemberSaveValidationAttribute(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService, IMemberTypeService memberTypeService, IMemberService memberService, IShortStringHelper shortStringHelper) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity)); - _textService = textService ?? throw new ArgumentNullException(nameof(textService)); - _memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService)); - _memberService = memberService ?? throw new ArgumentNullException(nameof(memberService)); - _shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper)); - } - - public override void OnActionExecuting(HttpActionContext actionContext) - { - var model = (MemberSave)actionContext.ActionArguments["contentItem"]; - var contentItemValidator = new MemberSaveModelValidator(_logger, _webSecurity, _textService, _memberTypeService, _memberService, _shortStringHelper); - //now do each validation step - if (contentItemValidator.ValidateExistingContent(model, actionContext)) - if (contentItemValidator.ValidateProperties(model, model, actionContext)) - contentItemValidator.ValidatePropertiesData(model, model, model.PropertyCollectionDto, actionContext.ModelState); - } - } -} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 8db1e9ea76..3a3a015373 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -256,7 +256,6 @@ - @@ -317,7 +316,6 @@ - @@ -361,7 +359,6 @@ - diff --git a/src/Umbraco.Web/WebApi/Filters/OutgoingNoHyphenGuidFormatAttribute.cs b/src/Umbraco.Web/WebApi/Filters/OutgoingNoHyphenGuidFormatAttribute.cs deleted file mode 100644 index 1685171cbb..0000000000 --- a/src/Umbraco.Web/WebApi/Filters/OutgoingNoHyphenGuidFormatAttribute.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Linq; -using System.Net.Http.Formatting; -using System.Web.Http.Controllers; - -namespace Umbraco.Web.WebApi.Filters -{ - internal sealed class OutgoingNoHyphenGuidFormatAttribute : Attribute, IControllerConfiguration - { - public OutgoingNoHyphenGuidFormatAttribute() - { - } - - public void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) - { - var jsonFormatter = controllerSettings.Formatters.OfType(); - foreach (var r in jsonFormatter) - { - r.SerializerSettings.Converters.Add(new GuidNoHyphenConverter()); - } - } - - } -} diff --git a/src/Umbraco.Web/WebApi/GuidNoHyphenConverter.cs b/src/Umbraco.Web/WebApi/GuidNoHyphenConverter.cs deleted file mode 100644 index dd5465732f..0000000000 --- a/src/Umbraco.Web/WebApi/GuidNoHyphenConverter.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using Newtonsoft.Json; -using Umbraco.Core; - -namespace Umbraco.Web.WebApi -{ - /// - /// A custom converter for GUID's to format without hyphens - /// - internal class GuidNoHyphenConverter : JsonConverter - { - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) - { - switch (reader.TokenType) - { - case JsonToken.Null: - return Guid.Empty; - case JsonToken.String: - var guidAttempt = reader.Value.TryConvertTo(); - if (guidAttempt.Success) - { - return guidAttempt.Result; - } - throw new FormatException("Could not convert " + reader.Value + " to a GUID"); - default: - throw new ArgumentException("Invalid token type"); - } - } - - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - writer.WriteValue(Guid.Empty.Equals(value) ? Guid.Empty.ToString("N") : ((Guid)value).ToString("N")); - } - - public override bool CanConvert(Type objectType) - { - return typeof(Guid) == objectType; - } - } -}