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)