From 4d31512ef071cd9de9f234b946dc0d0fa79bd095 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 18 Nov 2013 14:25:08 +1100 Subject: [PATCH] Custom membership provider's now working in the back office UI - have created intial functionality to allow custom properties with custom membership providers (the creation already performs the link if the Member member type is available). Updating also works too, need to clean up some code though as it's fairly messy. --- src/Umbraco.Core/Models/Member.cs | 10 + .../Models/Membership/MembershipExtensions.cs | 22 ++ .../Models/Membership/MembershipScenario.cs | 33 ++ src/Umbraco.Core/Services/MemberService.cs | 21 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../src/views/member/edit.html | 2 +- src/Umbraco.Web/Editors/MemberController.cs | 289 +++++++++++++----- .../Models/ContentEditing/MemberDisplay.cs | 4 + .../Models/Mapping/MemberModelMapper.cs | 50 +++ .../Providers/MembersMembershipProvider.cs | 2 +- src/Umbraco.Web/Trees/MemberTreeController.cs | 6 +- .../WebApi/Binders/MemberBinder.cs | 126 +++++++- 12 files changed, 460 insertions(+), 106 deletions(-) create mode 100644 src/Umbraco.Core/Models/Membership/MembershipScenario.cs diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs index f09b3a126c..2f0e96f294 100644 --- a/src/Umbraco.Core/Models/Member.cs +++ b/src/Umbraco.Core/Models/Member.cs @@ -117,6 +117,10 @@ namespace Umbraco.Core.Models [DataMember] public IEnumerable Groups { get; set; } + //TODO: When get/setting all of these properties we MUST: + // * Check if we are using the umbraco membership provider, if so then we need to use the configured fields - not the explicit fields below + // * If any of the fields don't exist, what should we do? Currently it will throw an exception! + /// /// Gets or sets the Password Question /// @@ -201,6 +205,7 @@ namespace Umbraco.Core.Models if (Properties[Constants.Conventions.Member.IsApproved].Value is bool) return (bool)Properties[Constants.Conventions.Member.IsApproved].Value; + //TODO: Use TryConvertTo instead return (bool)Convert.ChangeType(Properties[Constants.Conventions.Member.IsApproved].Value, typeof(bool)); } set @@ -227,6 +232,7 @@ namespace Umbraco.Core.Models if (Properties[Constants.Conventions.Member.IsLockedOut].Value is bool) return (bool)Properties[Constants.Conventions.Member.IsLockedOut].Value; + //TODO: Use TryConvertTo instead return (bool)Convert.ChangeType(Properties[Constants.Conventions.Member.IsLockedOut].Value, typeof(bool)); } set @@ -253,6 +259,7 @@ namespace Umbraco.Core.Models if (Properties[Constants.Conventions.Member.LastLoginDate].Value is DateTime) return (DateTime)Properties[Constants.Conventions.Member.LastLoginDate].Value; + //TODO: Use TryConvertTo instead return (DateTime)Convert.ChangeType(Properties[Constants.Conventions.Member.LastLoginDate].Value, typeof(DateTime)); } set @@ -279,6 +286,7 @@ namespace Umbraco.Core.Models if (Properties[Constants.Conventions.Member.LastPasswordChangeDate].Value is DateTime) return (DateTime)Properties[Constants.Conventions.Member.LastPasswordChangeDate].Value; + //TODO: Use TryConvertTo instead return (DateTime)Convert.ChangeType(Properties[Constants.Conventions.Member.LastPasswordChangeDate].Value, typeof(DateTime)); } set @@ -305,6 +313,7 @@ namespace Umbraco.Core.Models if (Properties[Constants.Conventions.Member.LastLockoutDate].Value is DateTime) return (DateTime)Properties[Constants.Conventions.Member.LastLockoutDate].Value; + //TODO: Use TryConvertTo instead return (DateTime)Convert.ChangeType(Properties[Constants.Conventions.Member.LastLockoutDate].Value, typeof(DateTime)); } set @@ -332,6 +341,7 @@ namespace Umbraco.Core.Models if (Properties[Constants.Conventions.Member.FailedPasswordAttempts].Value is int) return (int)Properties[Constants.Conventions.Member.FailedPasswordAttempts].Value; + //TODO: Use TryConvertTo instead return (int)Convert.ChangeType(Properties[Constants.Conventions.Member.FailedPasswordAttempts].Value, typeof(int)); } set diff --git a/src/Umbraco.Core/Models/Membership/MembershipExtensions.cs b/src/Umbraco.Core/Models/Membership/MembershipExtensions.cs index e88e807318..df0ee1529a 100644 --- a/src/Umbraco.Core/Models/Membership/MembershipExtensions.cs +++ b/src/Umbraco.Core/Models/Membership/MembershipExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Web.Security; +using Umbraco.Core.Services; namespace Umbraco.Core.Models.Membership { @@ -21,5 +22,26 @@ namespace Umbraco.Core.Models.Membership throw new NotImplementedException(); } + + private static MembershipScenario? _scenario = null; + /// + /// Returns the currently configured membership scenario for members in umbraco + /// + /// + internal static MembershipScenario GetMembershipScenario(this IMemberService memberService) + { + if (_scenario.HasValue == false) + { + if (System.Web.Security.Membership.Provider.Name == Constants.Conventions.Member.UmbracoMemberProviderName) + { + return MembershipScenario.NativeUmbraco; + } + var memberType = ApplicationContext.Current.Services.MemberTypeService.GetMemberType(Constants.Conventions.MemberTypes.Member); + return memberType != null + ? MembershipScenario.CustomProviderWithUmbracoLink + : MembershipScenario.StandaloneCustomProvider; + } + return _scenario.Value; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/Membership/MembershipScenario.cs b/src/Umbraco.Core/Models/Membership/MembershipScenario.cs new file mode 100644 index 0000000000..d198501321 --- /dev/null +++ b/src/Umbraco.Core/Models/Membership/MembershipScenario.cs @@ -0,0 +1,33 @@ +namespace Umbraco.Core.Models.Membership +{ + + /// + /// How membership is implemented in the current install. + /// + public enum MembershipScenario + { + /// + /// The member is based on the native Umbraco members (IMember + Umbraco membership provider) + /// + /// + /// This supports custom member properties + /// + NativeUmbraco, + + /// + /// The member is based on a custom member provider but it is linked to an IMember + /// + /// + /// This supports custom member properties (but that is not enabled yet) + /// + CustomProviderWithUmbracoLink, + + /// + /// The member is based purely on a custom member provider and is not linked to umbraco data + /// + /// + /// This does not support custom member properties. + /// + StandaloneCustomProvider + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 379cbc9d16..3b6a678ee6 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -401,7 +401,7 @@ namespace Umbraco.Core.Services /// A helper method that will create a basic/generic member for use with a generic membership provider /// /// - internal static IMember CreateGenericMembershipProviderMember() + internal static IMember CreateGenericMembershipProviderMember(string name, string email, string username, string password) { var identity = int.MaxValue; @@ -410,7 +410,7 @@ namespace Umbraco.Core.Services { Name = "Membership", Id = --identity - }; + }; propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) { Alias = Constants.Conventions.Member.Comments, @@ -418,20 +418,6 @@ namespace Umbraco.Core.Services SortOrder = 0, Id = --identity }); - propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Nvarchar) - { - Alias = Constants.Conventions.Member.FailedPasswordAttempts, - Name = Constants.Conventions.Member.FailedPasswordAttemptsLabel, - SortOrder = 1, - Id = --identity - }); - propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.NoEditAlias, DataTypeDatabaseType.Nvarchar) - { - Alias = Constants.Conventions.Member.IsApproved, - Name = Constants.Conventions.Member.IsApprovedLabel, - SortOrder = 2, - Id = --identity - }); propGroup.PropertyTypes.Add(new PropertyType(Constants.PropertyEditors.TrueFalseAlias, DataTypeDatabaseType.Integer) { Alias = Constants.Conventions.Member.IsApproved, @@ -467,9 +453,10 @@ namespace Umbraco.Core.Services SortOrder = 7, Id = --identity }); + memType.PropertyGroups.Add(propGroup); - return new Member("", memType); + return new Member(name, email, username, password, -1, memType); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 6d577a3d5e..0f8de9761a 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -321,6 +321,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/member/edit.html b/src/Umbraco.Web.UI.Client/src/views/member/edit.html index 9a11f3a757..22eae98b4b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/edit.html @@ -6,7 +6,7 @@ -
+
+ /// Returns the currently configured membership scenario for members in umbraco + /// + /// + protected MembershipScenario MembershipScenario + { + get { return Services.MemberService.GetMembershipScenario(); } + } + /// /// Gets the content json for the member /// @@ -60,21 +70,48 @@ namespace Umbraco.Web.Editors /// public MemberDisplay GetByKey(Guid key) { - if (Membership.Provider.Name == Constants.Conventions.Member.UmbracoMemberProviderName) + MembershipUser foundMembershipMember; + MemberDisplay display; + IMember foundMember; + switch (MembershipScenario) { - var foundMember = Services.MemberService.GetByKey(key); - if (foundMember == null) - { - HandleContentNotFound(key); - } - return Mapper.Map(foundMember); - } - else - { - //TODO: Support this - throw new HttpResponseException(Request.CreateValidationErrorResponse("Editing member with a non-umbraco membership provider is currently not supported")); - } + case MembershipScenario.NativeUmbraco: + foundMember = Services.MemberService.GetByKey(key); + if (foundMember == null) + { + HandleContentNotFound(key); + } + return Mapper.Map(foundMember); + case MembershipScenario.CustomProviderWithUmbracoLink: + //TODO: Support editing custom properties for members with a custom membership provider here. + + //foundMember = Services.MemberService.GetByKey(key); + //if (foundMember == null) + //{ + // HandleContentNotFound(key); + //} + //foundMembershipMember = Membership.GetUser(key, false); + //if (foundMembershipMember == null) + //{ + // HandleContentNotFound(key); + //} + + //display = Mapper.Map(foundMembershipMember); + ////map the name over + //display.Name = foundMember.Name; + //return display; + + case MembershipScenario.StandaloneCustomProvider: + default: + foundMembershipMember = Membership.GetUser(key, false); + if (foundMembershipMember == null) + { + HandleContentNotFound(key); + } + display = Mapper.Map(foundMembershipMember); + return display; + } } /// @@ -84,31 +121,31 @@ namespace Umbraco.Web.Editors /// public MemberDisplay GetEmpty(string contentTypeAlias = null) { - //if this is null and we are in umbraco member mode we cannot continue, return not found - if (global::umbraco.cms.businesslogic.member.Member.InUmbracoMemberMode()) + IMember emptyContent; + switch (MembershipScenario) { - if (contentTypeAlias == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } + case MembershipScenario.NativeUmbraco: + if (contentTypeAlias == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } - var contentType = Services.MemberTypeService.GetMemberType(contentTypeAlias); - if (contentType == null) - { - throw new HttpResponseException(HttpStatusCode.NotFound); - } + var contentType = Services.MemberTypeService.GetMemberType(contentTypeAlias); + if (contentType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } - IMember emptyContent = new Member("", contentType); - emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters); - return Mapper.Map(emptyContent); - - } - else - { - //we need to return a scaffold of a 'simple' member - basically just what a membership provider can edit - IMember emptyContent = MemberService.CreateGenericMembershipProviderMember(); - emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters); - return Mapper.Map(emptyContent); + emptyContent = new Member("", contentType); + emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters); + return Mapper.Map(emptyContent); + case MembershipScenario.CustomProviderWithUmbracoLink: + case MembershipScenario.StandaloneCustomProvider: + default: + //we need to return a scaffold of a 'simple' member - basically just what a membership provider can edit + emptyContent = MemberService.CreateGenericMembershipProviderMember("", "", "", ""); + emptyContent.AdditionalData["NewPassword"] = Membership.GeneratePassword(Membership.MinRequiredPasswordLength, Membership.MinRequiredNonAlphanumericCharacters); + return Mapper.Map(emptyContent); } } @@ -122,11 +159,6 @@ namespace Umbraco.Web.Editors [ModelBinder(typeof(MemberBinder))] MemberSave contentItem) { - //TODO : Support this! - if (Membership.Provider.Name != Constants.Conventions.Member.UmbracoMemberProviderName) - { - throw new NotSupportedException("Currently the member editor does not support providers that are not the default Umbraco membership provider "); - } //If we've reached here it means: // * Our model has been bound @@ -134,7 +166,19 @@ namespace Umbraco.Web.Editors // * 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 - + + //This is a special case for when we're not using the umbraco membership provider - when this is the case + // we will not have a ContentTypeAlias set which means the model state will be invalid but we don't care about that + // so we'll remove that model state value + if (MembershipScenario != MembershipScenario.NativeUmbraco) + { + ModelState.Remove("ContentTypeAlias"); + + //TODO: We're removing this because we are not displaying it but when we support the CustomProviderWithUmbracoLink scenario + // we will be able to have a real name associated so do not remove this state once that is implemented! + ModelState.Remove("Name"); + } + //map the properties to the persisted entity MapPropertyValues(contentItem); @@ -162,7 +206,7 @@ namespace Umbraco.Web.Editors break; case ContentSaveAction.SaveNew: MembershipCreateStatus status; - CreateWithUmbracoProvider(contentItem, out status); + CreateWithMembershipProvider(contentItem, out status); break; default: //we don't support anything else for members @@ -177,19 +221,24 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); } - //save the item - //NOTE: We are setting the password to NULL - this indicates to the system to not actually save the password - // so it will not get overwritten! - contentItem.PersistedContent.Password = null; - - //create/save the IMember - Services.MemberService.Save(contentItem.PersistedContent); + //save the IMember - + //TODO: When we support the CustomProviderWithUmbracoLink scenario, we'll need to save the custom properties for that here too + if (MembershipScenario == MembershipScenario.NativeUmbraco) + { + //save the item + //NOTE: We are setting the password to NULL - this indicates to the system to not actually save the password + // so it will not get overwritten! + contentItem.PersistedContent.Password = null; + + //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). var currGroups = Roles.GetRolesForUser(contentItem.PersistedContent.Username); //find the ones to remove and remove them - var toRemove = currGroups.Except(contentItem.Groups).ToArray(); + var toRemove = currGroups.Except(contentItem.Groups).ToArray(); if (toRemove.Any()) { Roles.RemoveUserFromRoles(contentItem.PersistedContent.Username, toRemove); @@ -209,7 +258,7 @@ namespace Umbraco.Web.Editors //return the updated model var display = Mapper.Map(contentItem.PersistedContent); - + //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 HandleInvalidModelState(display); @@ -271,6 +320,7 @@ namespace Umbraco.Web.Editors try { Membership.Provider.UpdateUser(membershipUser); + //re-map these values shouldReFetchMember = true; } catch (Exception ex) @@ -309,9 +359,18 @@ namespace Umbraco.Web.Editors //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"); } - + //password changes ? - if (contentItem.Password == null) return null; + if (contentItem.Password == null) + { + //If the provider has changed some values, these values need to be reflected in the member object + //that will get mapped to the display object + if (shouldReFetchMember) + { + RefetchMemberData(contentItem); + } + return null; + } var passwordChangeResult = Security.ChangePassword(membershipUser.UserName, contentItem.Password, Membership.Provider); if (passwordChangeResult.Success) @@ -320,10 +379,7 @@ namespace Umbraco.Web.Editors //that will get mapped to the display object if (shouldReFetchMember) { - //Go and re-fetch the persisted item - contentItem.PersistedContent = Services.MemberService.GetByUsername(contentItem.Username.Trim()); - //remap the values to save - MapPropertyValues(contentItem); + RefetchMemberData(contentItem); } //even if we weren't resetting this, it is the correct value (null), otherwise if we were resetting then it will contain the new pword @@ -339,6 +395,35 @@ namespace Umbraco.Web.Editors return null; } + /// + /// Re-fetches the database data to map to the PersistedContent object and re-maps the posted properties so that the display object is up-to-date + /// + /// + /// + /// This is done during an update if the membership provider has changed some underlying data - we need to ensure that our model is consistent with that data + /// + private void RefetchMemberData(MemberSave contentItem) + { + switch (MembershipScenario) + { + case MembershipScenario.NativeUmbraco: + //Go and re-fetch the persisted item + contentItem.PersistedContent = Services.MemberService.GetByKey(contentItem.Key); + //remap the values to save + MapPropertyValues(contentItem); + break; + case MembershipScenario.CustomProviderWithUmbracoLink: + case MembershipScenario.StandaloneCustomProvider: + default: + var membershipUser = Membership.GetUser(contentItem.Key, false); + //Go and re-fetch the persisted item + contentItem.PersistedContent = Mapper.Map(membershipUser); + //remap the values to save + MapPropertyValues(contentItem); + break; + } + } + /// /// Quick check to see if the 'normal' settable properties for the membership provider have changed /// @@ -366,25 +451,75 @@ namespace Umbraco.Web.Editors /// /// /// + /// Depending on if the Umbraco membership provider is active or not, the process differs slightly: + /// + /// * If the umbraco membership provider is used - we create the membership user first with the membership provider, since + /// it's the umbraco membership provider, this writes to the umbraco tables. When that is complete we re-fetch the IMember + /// model data from the db. In this case we don't care what the provider user key is. + /// * If we're using a non-umbraco membership provider - we check if there is a 'Member' member type - if so + /// we create an empty IMember instance first (of type 'Member'), this gives us a unique ID (GUID) + /// that we then use to create the member in the custom membership provider. This acts as the link between Umbraco data and + /// the custom membership provider data. This gives us the ability to eventually have custom membership properties but still use + /// a custom memberhip provider. If there is no 'Member' member type, then we will simply just create the membership provider member + /// with no link to our data. + /// /// If this is successful, it will go and re-fetch the IMember from the db because it will now have an ID because the Umbraco provider /// uses the umbraco data store - then of course we need to re-map it to the saved property values. /// - private MembershipUser CreateWithUmbracoProvider(MemberSave contentItem, out MembershipCreateStatus status) + private MembershipUser CreateWithMembershipProvider(MemberSave contentItem, out MembershipCreateStatus status) { - //if we are creating a new one, create the member using the membership provider first + MembershipUser membershipUser; - //TODO: I think we should detect if the Umbraco membership provider is active, if so then we'll create the member first and the provider key doesn't matter - // but if we are using a 3rd party membership provider - then we should create our IMember first and use it's key as their provider user key! + switch (MembershipScenario) + { + case MembershipScenario.NativeUmbraco: + //We are using the umbraco membership provider, create the member using the membership provider first. + var umbracoMembershipProvider = (global::umbraco.providers.members.UmbracoMembershipProvider)Membership.Provider; + //TODO: We are not supporting q/a - passing in empty here + membershipUser = umbracoMembershipProvider.CreateUser( + contentItem.ContentTypeAlias, contentItem.Username, + contentItem.Password.NewPassword, + contentItem.Email, "", "", + contentItem.IsApproved, + Guid.NewGuid(), //since it's the umbraco provider, the user key here doesn't make any difference + out status); + break; + case MembershipScenario.CustomProviderWithUmbracoLink: + //We are using a custom membership provider, we'll create an empty IMember first to get the unique id to use + // as the provider user key. + //create it: + Services.MemberService.Save(contentItem.PersistedContent); - //NOTE: We are casting directly to the umbraco membership provider so we can specify the member type that we want to use! + //TODO: We are not supporting q/a - passing in empty here + membershipUser = Membership.CreateUser( + contentItem.Username, + contentItem.Password.NewPassword, + contentItem.Email, + "TEMP", //some membership provider's require something here even if q/a is disabled! + "TEMP", //some membership provider's require something here even if q/a is disabled! + contentItem.IsApproved, + contentItem.PersistedContent.Key, //custom membership provider, we'll link that based on the IMember unique id (GUID) + out status); - var umbracoMembershipProvider = (global::umbraco.providers.members.UmbracoMembershipProvider)Membership.Provider; - - //TODO: We are not supporting q/a - passing in empty here - var membershipUser = umbracoMembershipProvider.CreateUser( - contentItem.ContentTypeAlias, contentItem.Username, - contentItem.Password.NewPassword, - contentItem.Email, "", "", true, Guid.NewGuid(), out status); + break; + case MembershipScenario.StandaloneCustomProvider: + // we don't have a member type to use so we will just create the basic membership user with the provider with no + // link back to the umbraco data + + //TODO: We are not supporting q/a - passing in empty here + membershipUser = Membership.CreateUser( + contentItem.Username, + contentItem.Password.NewPassword, + contentItem.Email, + "TEMP", //some membership provider's require something here even if q/a is disabled! + "TEMP", //some membership provider's require something here even if q/a is disabled! + contentItem.IsApproved, + Guid.NewGuid(), //we'll just generate this since we have no way to relate it to umbraco data. + out status); + break; + default: + throw new ArgumentOutOfRangeException(); + } //TODO: Localize these! switch (status) @@ -395,13 +530,19 @@ namespace Umbraco.Web.Editors if (contentItem.Comments.IsNullOrWhiteSpace() == false) { membershipUser.Comment = contentItem.Comments; - umbracoMembershipProvider.UpdateUser(membershipUser); + Membership.UpdateUser(membershipUser); + } + + //if we're using the umbraco provider, we'll have to go re-fetch the IMember since the provider + // has now updated it separately but we need to maintain all the correct bound data + if (MembershipScenario == MembershipScenario.NativeUmbraco) + { + //Go and re-fetch the persisted item + contentItem.PersistedContent = Services.MemberService.GetByUsername(contentItem.Username.Trim()); + //remap the values to save + MapPropertyValues(contentItem); } - //Go and re-fetch the persisted item - contentItem.PersistedContent = Services.MemberService.GetByUsername(contentItem.Username.Trim()); - //remap the values to save - MapPropertyValues(contentItem); break; case MembershipCreateStatus.InvalidUserName: diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs index 28033d7558..a295e7718f 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Runtime.Serialization; using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; namespace Umbraco.Web.Models.ContentEditing { @@ -22,6 +23,9 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "email")] public string Email { get; set; } + [DataMember(Name = "membershipScenario")] + public MembershipScenario MembershipScenario { get; set; } + /// /// This is used to indicate how to map the membership provider properties to the save model, this mapping /// will change if a developer has opted to have custom member property aliases specified in their membership provider config, diff --git a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs index d0d3f7d2a7..a0671560f1 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs @@ -5,6 +5,8 @@ using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Mapping; +using Umbraco.Core.Models.Membership; +using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using umbraco; using System.Linq; @@ -18,6 +20,29 @@ namespace Umbraco.Web.Models.Mapping { public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { + //FROM MembershipUser TO MediaItemDisplay - used when using a non-umbraco membership provider + config.CreateMap() + .ConvertUsing(user => + { + var member = Mapper.Map(user); + return Mapper.Map(member); + }); + + //FROM MembershipUser TO IMember - used when using a non-umbraco membership provider + config.CreateMap() + .ConstructUsing(user => MemberService.CreateGenericMembershipProviderMember(user.UserName, user.Email, user.UserName, "")) + .ForMember(member => member.Comments, expression => expression.MapFrom(user => user.Comment)) + .ForMember(member => member.CreateDate, expression => expression.MapFrom(user => user.CreationDate)) + .ForMember(member => member.UpdateDate, expression => expression.MapFrom(user => user.LastActivityDate)) + .ForMember(member => member.LastPasswordChangeDate, expression => expression.MapFrom(user => user.LastPasswordChangedDate)) + .ForMember(member => member.Key, expression => expression.MapFrom(user => user.ProviderUserKey.TryConvertTo().Result)) + //This is a special case for password - we don't actually care what the password is but it either needs to be something or nothing + // so we'll set it to something if the member is actually created, otherwise nothing if it is a new member. + .ForMember(member => member.Password, expression => expression.MapFrom(user => user.CreationDate > DateTime.MinValue ? Guid.NewGuid().ToString("N") : "")) + //TODO: Support these eventually + .ForMember(member => member.PasswordQuestion, expression => expression.Ignore()) + .ForMember(member => member.PasswordAnswer, expression => expression.Ignore()); + //FROM IMember TO MediaItemDisplay config.CreateMap() .ForMember( @@ -37,6 +62,8 @@ namespace Umbraco.Web.Models.Mapping expression => expression.ResolveUsing()) .ForMember(display => display.MemberProviderFieldMapping, expression => expression.ResolveUsing()) + .ForMember(display => display.MembershipScenario, + expression => expression.ResolveUsing(new MembershipScenarioMappingResolver(new Lazy(() => applicationContext.Services.MemberTypeService)))) .AfterMap(MapGenericCustomProperties); //FROM IMember TO ContentItemBasic @@ -216,6 +243,7 @@ namespace Umbraco.Web.Models.Mapping //This is kind of a hack because a developer is supposed to be allowed to set their property editor - would have been much easier // if we just had all of the membeship provider fields on the member table :( + // TODO: But is there a way to map the IMember.IsLockedOut to the property ? i dunno. var isLockedOutProperty = result.SelectMany(x => x.Properties).FirstOrDefault(x => x.Alias == umbracoProvider.LockPropertyTypeAlias); if (isLockedOutProperty != null && isLockedOutProperty.Value.ToString() != "1") { @@ -230,6 +258,28 @@ namespace Umbraco.Web.Models.Mapping } } + internal class MembershipScenarioMappingResolver : ValueResolver + { + private readonly Lazy _memberTypeService; + + public MembershipScenarioMappingResolver(Lazy memberTypeService) + { + _memberTypeService = memberTypeService; + } + + protected override MembershipScenario ResolveCore(IMember source) + { + if (Membership.Provider.Name == Constants.Conventions.Member.UmbracoMemberProviderName) + { + return MembershipScenario.NativeUmbraco; + } + var memberType = _memberTypeService.Value.GetMemberType(Constants.Conventions.MemberTypes.Member); + return memberType != null + ? MembershipScenario.CustomProviderWithUmbracoLink + : MembershipScenario.StandaloneCustomProvider; + } + } + /// /// A resolver to map the provider field aliases /// diff --git a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs index dc42ef5bf3..4d08403044 100644 --- a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs @@ -863,4 +863,4 @@ namespace Umbraco.Web.Security.Providers #endregion } -} +} diff --git a/src/Umbraco.Web/Trees/MemberTreeController.cs b/src/Umbraco.Web/Trees/MemberTreeController.cs index 4ba3e10b62..c740ccda5f 100644 --- a/src/Umbraco.Web/Trees/MemberTreeController.cs +++ b/src/Umbraco.Web/Trees/MemberTreeController.cs @@ -36,7 +36,7 @@ namespace Umbraco.Web.Trees nodes.Add(folder); } //list out 'Others' if the membership provider is umbraco - if (Member.InUmbracoMemberMode()) + if (Membership.Provider.Name == Constants.Conventions.Member.UmbracoMemberProviderName) { var folder = CreateTreeNode("others", id, queryStrings, "Others", "icon-folder-close", true); folder.NodeType = "member-folder"; @@ -48,7 +48,7 @@ namespace Umbraco.Web.Trees //if it is a letter if (id.Length == 1 && char.IsLower(id, 0)) { - if (Member.InUmbracoMemberMode()) + if (Membership.Provider.Name == Constants.Conventions.Member.UmbracoMemberProviderName) { //get the members from our member data layer nodes.AddRange( @@ -82,7 +82,7 @@ namespace Umbraco.Web.Trees if (id == Constants.System.Root.ToInvariantString()) { // root actions - if (Member.InUmbracoMemberMode()) + if (Membership.Provider.Name == Constants.Conventions.Member.UmbracoMemberProviderName) { //set default menu.DefaultMenuAlias = ActionNew.Instance.Alias; diff --git a/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs b/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs index 071c95fa56..c04b556e58 100644 --- a/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs @@ -1,13 +1,16 @@ using System; +using System.Collections.Generic; using System.Web.Http.Controllers; using System.Web.Http.ModelBinding; using System.Web.Security; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.WebApi.Filters; using System.Linq; +using Umbraco.Core.Models.Membership; namespace Umbraco.Web.WebApi.Binders { @@ -31,12 +34,63 @@ namespace Umbraco.Web.WebApi.Binders return new MemberValidationHelper(); } + /// + /// Returns an IMember instance used to bind values to and save (depending on the membership scenario) + /// + /// + /// protected override IMember GetExisting(MemberSave model) { - var member = ApplicationContext.Services.MemberService.GetByKey(model.Key); + var scenario = ApplicationContext.Services.MemberService.GetMembershipScenario(); + switch (scenario) + { + case MembershipScenario.NativeUmbraco: + return GetExisting(model.Key); + case MembershipScenario.CustomProviderWithUmbracoLink: + case MembershipScenario.StandaloneCustomProvider: + default: + var membershipUser = Membership.GetUser(model.Key, false); + if (membershipUser == null) + { + throw new InvalidOperationException("Could not find member with key " + model.Key); + } + + //TODO: Support this scenario! + //if (scenario == MembershipScenario.CustomProviderWithUmbracoLink) + //{ + // //if there's a 'Member' type then we should be able to just go get it from the db since it was created with a link + // // to our data. + // var memberType = ApplicationContext.Services.MemberTypeService.GetMemberType(Constants.Conventions.MemberTypes.Member); + // if (memberType != null) + // { + // var existing = GetExisting(model.Key); + // FilterContentTypeProperties(existing.ContentType, existing.ContentType.PropertyTypes.Select(x => x.Alias).ToArray()); + // } + //} + + //generate a member for a generic membership provider + //NOTE: We don't care about the password here, so just generate something + //var member = MemberService.CreateGenericMembershipProviderMember(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N")); + + //var convertResult = membershipUser.ProviderUserKey.TryConvertTo(); + //if (convertResult.Success == false) + //{ + // throw new InvalidOperationException("Only membership providers that store a GUID as their ProviderUserKey are supported" + model.Key); + //} + //member.Key = convertResult.Result; + + var member = Mapper.Map(membershipUser); + + return member; + } + } + + private IMember GetExisting(Guid key) + { + var member = ApplicationContext.Services.MemberService.GetByKey(key); if (member == null) { - throw new InvalidOperationException("Could not find member with key " + model.Key); + throw new InvalidOperationException("Could not find member with key " + key); } //remove all membership properties, these values are set with the membership provider. @@ -49,26 +103,78 @@ namespace Umbraco.Web.WebApi.Binders return member; } + /// + /// Gets an instance of IMember used when creating a member + /// + /// + /// + /// + /// Depending on whether a custom membership provider is configured this will return different results. + /// protected override IMember CreateNew(MemberSave model) { - var contentType = ApplicationContext.Services.MemberTypeService.GetMemberType(model.ContentTypeAlias); - if (contentType == null) + if (Membership.Provider.Name == Constants.Conventions.Member.UmbracoMemberProviderName) { - throw new InvalidOperationException("No member type found wth alias " + model.ContentTypeAlias); - } + var contentType = ApplicationContext.Services.MemberTypeService.GetMemberType(model.ContentTypeAlias); + if (contentType == null) + { + throw new InvalidOperationException("No member type found wth alias " + model.ContentTypeAlias); + } + //remove all membership properties, these values are set with the membership provider. + FilterMembershipProviderProperties(contentType); + + //return the new member with the details filled in + return new Member(model.Name, model.Email, model.Username, model.Password.NewPassword, -1, contentType); + } + else + { + //A custom membership provider is configured + + //NOTE: Below we are assigning the password to just a new GUID because we are not actually storing the password, however that + // field is mandatory in the database so we need to put something there. + + //If the default Member type exists, we'll use that to create the IMember - that way we can associate the custom membership + // provider to our data - eventually we can support editing custom properties with a custom provider. + var memberType = ApplicationContext.Services.MemberTypeService.GetMemberType(Constants.Conventions.MemberTypes.Member); + if (memberType != null) + { + FilterContentTypeProperties(memberType, memberType.PropertyTypes.Select(x => x.Alias).ToArray()); + return new Member(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N"), -1, memberType); + } + + //generate a member for a generic membership provider + var member = MemberService.CreateGenericMembershipProviderMember(model.Name, model.Email, model.Username, Guid.NewGuid().ToString("N")); + //we'll just remove all properties here otherwise we'll end up with validation errors, we don't want to persist any property data anyways + // in this case. + FilterContentTypeProperties(member.ContentType, member.ContentType.PropertyTypes.Select(x => x.Alias).ToArray()); + return member; + } + } + + /// + /// This will remove all of the special membership provider properties which were required to display the property editors + /// for editing - but the values have been mapped back ot the MemberSave object directly - we don't want to keep these properties + /// on the IMember because they will attempt to be persisted which we don't want since they might not even exist. + /// + /// + private void FilterMembershipProviderProperties(IContentTypeBase contentType) + { //remove all membership properties, these values are set with the membership provider. var exclude = Constants.Conventions.Member.StandardPropertyTypeStubs.Select(x => x.Value.Alias).ToArray(); + FilterContentTypeProperties(contentType, exclude); + } + + private void FilterContentTypeProperties(IContentTypeBase contentType, IEnumerable exclude) + { + //remove all properties based on the exclusion list foreach (var remove in exclude) { if (contentType.PropertyTypeExists(remove)) { - contentType.RemovePropertyType(remove); + contentType.RemovePropertyType(remove); } } - - //return the new member with the details filled in - return new Member(model.Name, model.Email, model.Username, model.Password.NewPassword, -1, contentType); } protected override ContentItemDto MapFromPersisted(MemberSave model)