From a87b37d6ca9ccab42c808f92c84d3b143fbd0042 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Oct 2013 09:35:23 +1100 Subject: [PATCH 01/10] Cleans up more of the IContentService and ensures that we have PublishStatus returned from the publishing methods instead of hiding them internally. --- src/Umbraco.Core/Publishing/PublishStatus.cs | 2 +- .../Publishing/PublishStatusType.cs | 2 +- src/Umbraco.Core/Services/ContentService.cs | 104 +++++++++++------- src/Umbraco.Core/Services/IContentService.cs | 30 +++++ src/umbraco.cms/businesslogic/web/Document.cs | 21 ++-- 5 files changed, 107 insertions(+), 52 deletions(-) diff --git a/src/Umbraco.Core/Publishing/PublishStatus.cs b/src/Umbraco.Core/Publishing/PublishStatus.cs index aee9a1fafe..865f00c58c 100644 --- a/src/Umbraco.Core/Publishing/PublishStatus.cs +++ b/src/Umbraco.Core/Publishing/PublishStatus.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Publishing /// /// The result of publishing a content item /// - internal class PublishStatus + public class PublishStatus { public PublishStatus() { diff --git a/src/Umbraco.Core/Publishing/PublishStatusType.cs b/src/Umbraco.Core/Publishing/PublishStatusType.cs index c3cb76e245..0d9ffcfa02 100644 --- a/src/Umbraco.Core/Publishing/PublishStatusType.cs +++ b/src/Umbraco.Core/Publishing/PublishStatusType.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Publishing /// /// Anything less than 10 = Success! /// - internal enum PublishStatusType + public enum PublishStatusType { /// /// The publishing was successful. diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 64bcaa4d8e..1df2133891 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -627,28 +627,53 @@ namespace Umbraco.Core.Services /// The to publish /// Optional Id of the User issueing the publishing /// True if publishing succeeded, otherwise False + [Obsolete("Use PublishWithStatus instead, that method will provide more detailed information on the outcome")] public bool Publish(IContent content, int userId = 0) { var result = SaveAndPublishDo(content, userId); return result.Success; } + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// True if publishing succeeded, otherwise False + public Attempt PublishWithStatus(IContent content, int userId = 0) + { + return SaveAndPublishDo(content, userId); + } + /// /// Publishes a object and all its children /// /// The to publish along with its children /// Optional Id of the User issueing the publishing /// True if publishing succeeded, otherwise False + [Obsolete("Use PublishWithChildrenWithStatus instead, that method will provide more detailed information on the outcome and also allows the includeUnpublished flag")] public bool PublishWithChildren(IContent content, int userId = 0) { var result = PublishWithChildrenDo(content, userId, true); - + //This used to just return false only when the parent content failed, otherwise would always return true so we'll // do the same thing for the moment - if (!result.Any(x => x.Result.ContentItem.Id == content.Id)) - return false; + if (!result.Any(x => x.Result.ContentItem.Id == content.Id)) + return false; - return result.Single(x => x.Result.ContentItem.Id == content.Id).Success; + return result.Single(x => x.Result.ContentItem.Id == content.Id).Success; + } + + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// set to true if you want to also publish children that are currently unpublished + /// True if publishing succeeded, otherwise False + public IEnumerable> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false) + { + return PublishWithChildrenDo(content, userId, includeUnpublished); } /// @@ -669,12 +694,25 @@ namespace Umbraco.Core.Services /// Optional Id of the User issueing the publishing /// Optional boolean indicating whether or not to raise save events. /// True if publishing succeeded, otherwise False + [Obsolete("Use SaveAndPublishWithStatus instead, that method will provide more detailed information on the outcome")] public bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true) { var result = SaveAndPublishDo(content, userId, raiseEvents); return result.Success; } + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + public Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true) + { + return SaveAndPublishDo(content, userId, raiseEvents); + } + /// /// Saves a single object /// @@ -1329,42 +1367,30 @@ namespace Umbraco.Core.Services } #region Internal Methods - - /// - /// Internal method that Publishes a single object for legacy purposes. - /// - /// The to publish - /// Optional Id of the User issueing the publishing - /// True if publishing succeeded, otherwise False - internal Attempt PublishInternal(IContent content, int userId = 0) - { - return SaveAndPublishDo(content, userId); - } - /// - /// Internal method that Publishes a object and all its children for legacy purposes. - /// - /// The to publish along with its children - /// Optional Id of the User issueing the publishing - /// If set to true, this will also publish descendants that are completely unpublished, normally this will only publish children that have previously been published - /// True if publishing succeeded, otherwise False - internal IEnumerable> PublishWithChildrenInternal( - IContent content, int userId = 0, bool includeUnpublished = false) - { - return PublishWithChildrenDo(content, userId, includeUnpublished); - } + ///// + ///// Internal method that Publishes a single object for legacy purposes. + ///// + ///// The to publish + ///// Optional Id of the User issueing the publishing + ///// True if publishing succeeded, otherwise False + //internal Attempt PublishInternal(IContent content, int userId = 0) + //{ + // return SaveAndPublishDo(content, userId); + //} - /// - /// Saves and Publishes a single object - /// - /// The to save and publish - /// Optional Id of the User issueing the publishing - /// Optional boolean indicating whether or not to raise save events. - /// True if publishing succeeded, otherwise False - internal Attempt SaveAndPublishInternal(IContent content, int userId = 0, bool raiseEvents = true) - { - return SaveAndPublishDo(content, userId, raiseEvents); - } + ///// + ///// Internal method that Publishes a object and all its children for legacy purposes. + ///// + ///// The to publish along with its children + ///// Optional Id of the User issueing the publishing + ///// If set to true, this will also publish descendants that are completely unpublished, normally this will only publish children that have previously been published + ///// True if publishing succeeded, otherwise False + //internal IEnumerable> PublishWithChildrenInternal( + // IContent content, int userId = 0, bool includeUnpublished = false) + //{ + // return PublishWithChildrenDo(content, userId, includeUnpublished); + //} /// /// Gets a collection of descendants by the first Parent. @@ -1462,7 +1488,7 @@ namespace Umbraco.Core.Services /// then the list will only contain one status item, otherwise it will contain status items for it and all of it's descendants that /// are to be published. /// - private IEnumerable> PublishWithChildrenDo( + private IEnumerable> PublishWithChildrenDo( IContent content, int userId = 0, bool includeUnpublished = false) { if (content == null) throw new ArgumentNullException("content"); diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 8ac8ade1fb..a265b15092 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; +using Umbraco.Core.Publishing; namespace Umbraco.Core.Services { @@ -246,16 +247,35 @@ namespace Umbraco.Core.Services /// The to publish /// Optional Id of the User issueing the publishing /// True if publishing succeeded, otherwise False + [Obsolete("Use PublishWithStatus instead, that method will provide more detailed information on the outcome")] bool Publish(IContent content, int userId = 0); + /// + /// Publishes a single object + /// + /// The to publish + /// Optional Id of the User issueing the publishing + /// The published status attempt + Attempt PublishWithStatus(IContent content, int userId = 0); + /// /// Publishes a object and all its children /// /// The to publish along with its children /// Optional Id of the User issueing the publishing /// True if publishing succeeded, otherwise False + [Obsolete("Use PublishWithChildrenWithStatus instead, that method will provide more detailed information on the outcome and also allows the includeUnpublished flag")] bool PublishWithChildren(IContent content, int userId = 0); + /// + /// Publishes a object and all its children + /// + /// The to publish along with its children + /// Optional Id of the User issueing the publishing + /// + /// The list of statuses for all published items + IEnumerable> PublishWithChildrenWithStatus(IContent content, int userId = 0, bool includeUnpublished = false); + /// /// UnPublishes a single object /// @@ -271,8 +291,18 @@ namespace Umbraco.Core.Services /// Optional Id of the User issueing the publishing /// Optional boolean indicating whether or not to raise save events. /// True if publishing succeeded, otherwise False + [Obsolete("Use SaveAndPublishWithStatus instead, that method will provide more detailed information on the outcome")] bool SaveAndPublish(IContent content, int userId = 0, bool raiseEvents = true); + /// + /// Saves and Publishes a single object + /// + /// The to save and publish + /// Optional Id of the User issueing the publishing + /// Optional boolean indicating whether or not to raise save events. + /// True if publishing succeeded, otherwise False + Attempt SaveAndPublishWithStatus(IContent content, int userId = 0, bool raiseEvents = true); + /// /// Permanently deletes an object. /// diff --git a/src/umbraco.cms/businesslogic/web/Document.cs b/src/umbraco.cms/businesslogic/web/Document.cs index eb5b97160e..32d61a3fe3 100644 --- a/src/umbraco.cms/businesslogic/web/Document.cs +++ b/src/umbraco.cms/businesslogic/web/Document.cs @@ -815,7 +815,7 @@ namespace umbraco.cms.businesslogic.web if (!e.Cancel) { - var result = ((ContentService)ApplicationContext.Current.Services.ContentService).PublishInternal(Content, u.Id); + var result = ApplicationContext.Current.Services.ContentService.PublishWithStatus(Content, u.Id); _published = result.Success; FireAfterPublish(e); @@ -831,8 +831,7 @@ namespace umbraco.cms.businesslogic.web [Obsolete("Obsolete, Use Umbraco.Core.Services.ContentService.PublishWithChildren()", false)] public bool PublishWithChildrenWithResult(User u) { - var result = ((ContentService)ApplicationContext.Current.Services.ContentService) - .PublishWithChildrenInternal(Content, u.Id, true); + var result = ApplicationContext.Current.Services.ContentService.PublishWithChildrenWithStatus(Content, u.Id, true); //This used to just return false only when the parent content failed, otherwise would always return true so we'll // do the same thing for the moment return result.Single(x => x.Result.ContentItem.Id == Id).Success; @@ -872,8 +871,8 @@ namespace umbraco.cms.businesslogic.web if (!e.Cancel) { - IEnumerable> publishedResults = ((ContentService)ApplicationContext.Current.Services.ContentService) - .PublishWithChildrenInternal(Content, u.Id); + IEnumerable> publishedResults = ApplicationContext.Current.Services.ContentService + .PublishWithChildrenWithStatus(Content, u.Id); FireAfterPublish(e); } @@ -889,8 +888,8 @@ namespace umbraco.cms.businesslogic.web if (!e.Cancel) { - publishedResults = ((ContentService) ApplicationContext.Current.Services.ContentService) - .PublishWithChildrenInternal(Content, userId, includeUnpublished); + publishedResults = ApplicationContext.Current.Services.ContentService + .PublishWithChildrenWithStatus(Content, userId, includeUnpublished); FireAfterPublish(e); } @@ -918,8 +917,8 @@ namespace umbraco.cms.businesslogic.web if (!publishArgs.Cancel) { //NOTE: The 'false' parameter will cause the PublishingStrategy events to fire which will ensure that the cache is refreshed. - result = ((ContentService)ApplicationContext.Current.Services.ContentService) - .SaveAndPublishInternal(Content, userId); + result = ApplicationContext.Current.Services.ContentService + .SaveAndPublishWithStatus(Content, userId); base.VersionDate = Content.UpdateDate; this.UpdateDate = Content.UpdateDate; @@ -1007,8 +1006,8 @@ namespace umbraco.cms.businesslogic.web if (!publishArgs.Cancel) { //NOTE: The 'false' parameter will cause the PublishingStrategy events to fire which will ensure that the cache is refreshed. - var result = ((ContentService)ApplicationContext.Current.Services.ContentService) - .SaveAndPublishInternal(Content, u.Id); + var result = ApplicationContext.Current.Services.ContentService + .SaveAndPublishWithStatus(Content, u.Id); base.VersionDate = Content.UpdateDate; this.UpdateDate = Content.UpdateDate; From 71e6ea70b29271d176379c03bb87d1eae98b7058 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Oct 2013 12:22:38 +1100 Subject: [PATCH 02/10] Updates member repository and tests to ensure that an existing password will not be replaced if the password specified is null or empty. --- src/Umbraco.Core/Models/Member.cs | 11 ++++ .../Repositories/MemberRepository.cs | 29 ++++++++-- .../Repositories/MemberRepositoryTest.cs | 57 +++++++++++++++++++ 3 files changed, 93 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs index 335beb9ee4..650f664254 100644 --- a/src/Umbraco.Core/Models/Member.cs +++ b/src/Umbraco.Core/Models/Member.cs @@ -20,6 +20,17 @@ namespace Umbraco.Core.Models private object _providerUserKey; private Type _userTypeKey; + /// + /// Constructor for creating a Member object + /// + /// Name of the content + /// ContentType for the current Content object + public Member(string name, IMemberType contentType) + : base(name, -1, contentType, new PropertyCollection()) + { + _contentType = contentType; + } + public Member(string name, string email, string username, string password, int parentId, IMemberType contentType) : base(name, parentId, contentType, new PropertyCollection()) { diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index 8854f169cd..dbaec8471a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -265,8 +265,10 @@ namespace Umbraco.Core.Persistence.Repositories //Updates Modified date ((Member)entity).UpdatingEntity(); + var dirtyEntity = (ICanBeDirty) entity; + //Look up parent to get and set the correct Path and update SortOrder if ParentId has changed - if (((ICanBeDirty)entity).IsPropertyDirty("ParentId")) + if (dirtyEntity.IsPropertyDirty("ParentId")) { var parent = Database.First("WHERE id = @ParentId", new { ParentId = ((IUmbracoEntity)entity).ParentId }); ((IUmbracoEntity)entity).Path = string.Concat(parent.Path, ",", entity.Id); @@ -302,8 +304,27 @@ namespace Umbraco.Core.Persistence.Repositories //Updates the current version - cmsContentVersion //Assumes a Version guid exists and Version date (modified date) has been set/updated Database.Update(dto.ContentVersionDto); - //Updates the cmsMember entry - Database.Update(dto); + + //Updates the cmsMember entry if it has changed + var changedCols = new List(); + if (dirtyEntity.IsPropertyDirty("Email")) + { + changedCols.Add("Email"); + } + if (dirtyEntity.IsPropertyDirty("Username")) + { + changedCols.Add("LoginName"); + } + // DO NOT update the password if it is null or empty + if (dirtyEntity.IsPropertyDirty("Password") && entity.Password.IsNullOrWhiteSpace() == false) + { + changedCols.Add("Password"); + } + //only update the changed cols + if (changedCols.Count > 0) + { + Database.Update(dto, changedCols); + } //TODO ContentType for the Member entity @@ -337,7 +358,7 @@ namespace Umbraco.Core.Persistence.Repositories UpdatePropertyTags(entity, _tagRepository); - ((ICanBeDirty)entity).ResetDirtyProperties(); + dirtyEntity.ResetDirtyProperties(); } protected override void PersistDeletedItem(IMember entity) diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index fa75070ad4..49a63a007e 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -189,6 +189,63 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void MemberRepository_Does_Not_Replace_Password_When_Null() + { + IMember sut; + var provider = new PetaPocoUnitOfWorkProvider(); + var unitOfWork = provider.GetUnitOfWork(); + MemberTypeRepository memberTypeRepository; + using (var repository = CreateRepository(unitOfWork, out memberTypeRepository)) + { + var memberType = MockedContentTypes.CreateSimpleMemberType(); + memberTypeRepository.AddOrUpdate(memberType); + unitOfWork.Commit(); + + var member = MockedMember.CreateSimpleContent(memberType, "Johnny Hefty", "johnny@example.com", "123", "hefty", -1); + repository.AddOrUpdate(member); + unitOfWork.Commit(); + + sut = repository.Get(member.Id); + //when the password is null it will not overwrite what is already there. + sut.Password = null; + repository.AddOrUpdate(sut); + unitOfWork.Commit(); + sut = repository.Get(member.Id); + + Assert.That(sut.Password, Is.EqualTo("123")); + } + } + + [Test] + public void MemberRepository_Can_Update_Email_And_Login_When_Changed() + { + IMember sut; + var provider = new PetaPocoUnitOfWorkProvider(); + var unitOfWork = provider.GetUnitOfWork(); + MemberTypeRepository memberTypeRepository; + using (var repository = CreateRepository(unitOfWork, out memberTypeRepository)) + { + var memberType = MockedContentTypes.CreateSimpleMemberType(); + memberTypeRepository.AddOrUpdate(memberType); + unitOfWork.Commit(); + + var member = MockedMember.CreateSimpleContent(memberType, "Johnny Hefty", "johnny@example.com", "123", "hefty", -1); + repository.AddOrUpdate(member); + unitOfWork.Commit(); + + sut = repository.Get(member.Id); + sut.Username = "This is new"; + sut.Email = "thisisnew@hello.com"; + repository.AddOrUpdate(sut); + unitOfWork.Commit(); + sut = repository.Get(member.Id); + + Assert.That(sut.Email, Is.EqualTo("thisisnew@hello.com")); + Assert.That(sut.Username, Is.EqualTo("This is new")); + } + } + [Test] public void Can_Create_Correct_Subquery() { From 0843388a4130ac1db23ef2e8a679f16bb703fcc0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Oct 2013 13:39:17 +1100 Subject: [PATCH 03/10] Fixes up and implements more if the IMemberService, repository and membership providers (both legacy and what seems to be the new one). --- src/Umbraco.Core/Constants-Conventions.cs | 4 ++ src/Umbraco.Core/Models/PagedResult.cs | 63 +++++++++++++++++++ .../Persistence/PetaPocoExtensions.cs | 14 +++++ .../Interfaces/IMemberRepository.cs | 2 + .../Repositories/MemberRepository.cs | 22 +++++++ src/Umbraco.Core/Properties/AssemblyInfo.cs | 1 + src/Umbraco.Core/Services/IMemberService.cs | 4 +- src/Umbraco.Core/Services/MemberService.cs | 8 +++ .../Providers/MembersMembershipProvider.cs | 13 +++- .../businesslogic/member/Member.cs | 4 +- .../members/MembersMembershipProvider.cs | 24 +++++-- .../umbraco.providers.csproj | 4 ++ 12 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 src/Umbraco.Core/Models/PagedResult.cs diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index c3916a2e87..660a8073c9 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -99,6 +99,10 @@ namespace Umbraco.Core /// public static class Member { + public static readonly string UmbracoMemberProviderName = "UmbracoMembershipProvider"; + + public static readonly string UmbracoRoleProviderName = "UmbracoRoleProvider"; + /// /// Property alias for a Members Password Question /// diff --git a/src/Umbraco.Core/Models/PagedResult.cs b/src/Umbraco.Core/Models/PagedResult.cs new file mode 100644 index 0000000000..2e35da9a96 --- /dev/null +++ b/src/Umbraco.Core/Models/PagedResult.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace Umbraco.Core.Models +{ + /// + /// Represents a paged result for a model collection + /// + /// + [DataContract(Name = "pagedCollection", Namespace = "")] + public class PagedResult + { + public PagedResult(long totalItems, long pageNumber, long pageSize) + { + TotalItems = totalItems; + PageNumber = pageNumber; + PageSize = pageSize; + + if (pageSize > 0) + { + TotalPages = (long) Math.Ceiling(totalItems/(Decimal) pageSize); + } + else + { + TotalPages = 1; + } + } + + [DataMember(Name = "pageNumber")] + public long PageNumber { get; private set; } + + [DataMember(Name = "pageSize")] + public long PageSize { get; private set; } + + [DataMember(Name = "totalPages")] + public long TotalPages { get; private set; } + + [DataMember(Name = "totalItems")] + public long TotalItems { get; private set; } + + [DataMember(Name = "items")] + public IEnumerable Items { get; set; } + + /// + /// Calculates the skip size based on the paged parameters specified + /// + /// + /// Returns 0 if the page number or page size is zero + /// + internal int SkipSize + { + get + { + if (PageNumber > 0 && PageSize > 0) + { + return Convert.ToInt32((PageNumber - 1)*PageSize); + } + return 0; + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs index eb10bf7f87..f249ca4577 100644 --- a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs +++ b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data; using System.Linq; +using System.Text.RegularExpressions; using Umbraco.Core.Logging; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence.DatabaseModelDefinitions; @@ -17,6 +18,19 @@ namespace Umbraco.Core.Persistence internal static event CreateTableEventHandler NewTable; + /// + /// This will escape single @ symbols for peta poco values so it doesn't think it's a parameter + /// + /// + /// + /// + public static string EscapeAtSymbols(this Database db, string value) + { + //this fancy regex will only match a single @ not a double, etc... + var regex = new Regex("(?(this Database db) where T : new() { diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs index db9a1b4f19..92a8b5365b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs @@ -11,5 +11,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// IEnumerable GetByMemberGroup(string groupName); + + IEnumerable GetMembersByEmails(params string[] emails); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index dbaec8471a..c9d716746e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -437,6 +437,28 @@ namespace Umbraco.Core.Persistence.Repositories return BuildFromDtos(dtos); } + public IEnumerable GetMembersByEmails(params string[] emails) + { + var sql = GetBaseQuery(false); + if (emails.Any()) + { + var statement = string.Join(" OR ", + emails.Select(x => + string.Format( + "cmsMember.Email='{0}'", + //we have to escape the @ symbol for petapoco to work!! with 2 @@ symbols + Database.EscapeAtSymbols(x)))); + sql.Where(statement); + } + sql.OrderByDescending(x => x.VersionDate); + + var dtos = + Database.Fetch( + new PropertyDataRelator().Map, sql); + + return BuildFromDtos(dtos); + } + private IMember BuildFromDto(List dtos) { if (dtos == null || dtos.Any() == false) diff --git a/src/Umbraco.Core/Properties/AssemblyInfo.cs b/src/Umbraco.Core/Properties/AssemblyInfo.cs index fabcb7fe98..2104841349 100644 --- a/src/Umbraco.Core/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Core/Properties/AssemblyInfo.cs @@ -40,6 +40,7 @@ using System.Security.Permissions; [assembly: InternalsVisibleTo("Concorde.Sync")] [assembly: InternalsVisibleTo("Umbraco.Belle")] [assembly: InternalsVisibleTo("Umbraco.VisualStudio")] +[assembly: InternalsVisibleTo("umbraco.providers")] //allow this to be mocked in our unit tests [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index 737e142c3b..004a66d151 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -14,7 +14,7 @@ namespace Umbraco.Core.Services IEnumerable GetMembersByMemberType(string memberTypeAlias); IEnumerable GetMembersByGroup(string memberGroupName); IEnumerable GetAllMembers(params int[] ids); - + //TODO: Need to get all members that start with a certain letter } @@ -37,5 +37,7 @@ namespace Umbraco.Core.Services void Delete(IMember membershipUser); void Save(IMember membershipUser); + + IEnumerable GetMembersByEmails(params string[] emails); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 914ef70e60..21cce31400 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -108,6 +108,14 @@ namespace Umbraco.Core.Services } } + public IEnumerable GetMembersByEmails(params string[] emails) + { + using (var repository = _repositoryFactory.CreateMemberRepository(_uowProvider.GetUnitOfWork())) + { + return repository.GetMembersByEmails(emails); + } + } + /// /// Gets a list of Members with a certain string property value /// diff --git a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs index b02ed4ecf2..0cc4146f85 100644 --- a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Specialized; using System.Configuration.Provider; +using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.RegularExpressions; @@ -8,6 +9,7 @@ using System.Web.Hosting; using System.Web.Security; using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Models; using Umbraco.Core.Services; using Umbraco.Core.Models.Membership; @@ -687,7 +689,16 @@ namespace Umbraco.Web.Security.Providers /// public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords) { - throw new System.NotImplementedException(); + var byEmail = MemberService.GetMembersByEmails(emailToMatch).ToArray(); + totalRecords = byEmail.Length; + var pagedResult = new PagedResult(totalRecords, pageIndex, pageSize); + + var collection = new MembershipUserCollection(); + foreach (var m in byEmail.Skip(pagedResult.SkipSize).Take(pageSize)) + { + collection.Add(m.AsConcreteMembershipUser()); + } + return collection; } #region Private methods diff --git a/src/umbraco.cms/businesslogic/member/Member.cs b/src/umbraco.cms/businesslogic/member/Member.cs index 26497187c5..3394144096 100644 --- a/src/umbraco.cms/businesslogic/member/Member.cs +++ b/src/umbraco.cms/businesslogic/member/Member.cs @@ -31,8 +31,8 @@ namespace umbraco.cms.businesslogic.member public class Member : Content { #region Constants and static members - public static readonly string UmbracoMemberProviderName = "UmbracoMembershipProvider"; - public static readonly string UmbracoRoleProviderName = "UmbracoRoleProvider"; + public static readonly string UmbracoMemberProviderName = Constants.Conventions.Member.UmbracoMemberProviderName; + public static readonly string UmbracoRoleProviderName = Constants.Conventions.Member.UmbracoRoleProviderName; public static readonly Guid _objectType = new Guid(Constants.ObjectTypes.Member); private static readonly object m_Locker = new object(); diff --git a/src/umbraco.providers/members/MembersMembershipProvider.cs b/src/umbraco.providers/members/MembersMembershipProvider.cs index 0572613d78..7c8fe8e63a 100644 --- a/src/umbraco.providers/members/MembersMembershipProvider.cs +++ b/src/umbraco.providers/members/MembersMembershipProvider.cs @@ -1,20 +1,26 @@ #region namespace using System; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Web.Security; using System.Configuration; +using Umbraco.Core; +using Umbraco.Core.Models; using umbraco.BusinessLogic; using System.Security.Cryptography; using System.Web.Util; using System.Collections.Specialized; using System.Configuration.Provider; using umbraco.cms.businesslogic; -using umbraco.cms.businesslogic.member; - using System.Security; using System.Security.Permissions; using System.Runtime.CompilerServices; +using Member = umbraco.cms.businesslogic.member.Member; +using MemberType = umbraco.cms.businesslogic.member.MemberType; +using Umbraco.Core.Models.Membership; +using User = umbraco.BusinessLogic.User; + #endregion namespace umbraco.providers.members @@ -439,7 +445,16 @@ namespace umbraco.providers.members /// public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords) { - throw new Exception("The method or operation is not implemented."); + var byEmail = ApplicationContext.Current.Services.MemberService.GetMembersByEmails(emailToMatch).ToArray(); + totalRecords = byEmail.Length; + var pagedResult = new PagedResult(totalRecords, pageIndex, pageSize); + + var collection = new MembershipUserCollection(); + foreach (var m in byEmail.Skip(pagedResult.SkipSize).Take(pageSize)) + { + collection.Add(m.AsConcreteMembershipUser()); + } + return collection; } /// @@ -751,7 +766,7 @@ namespace umbraco.providers.members return null; } - + /// /// Verifies that the specified user name and password exist in the data source. /// @@ -973,6 +988,7 @@ namespace umbraco.providers.members DateTime.Now, DateTime.Now, DateTime.Now); } } + #endregion } } diff --git a/src/umbraco.providers/umbraco.providers.csproj b/src/umbraco.providers/umbraco.providers.csproj index 7082890e58..41c1457c7d 100644 --- a/src/umbraco.providers/umbraco.providers.csproj +++ b/src/umbraco.providers/umbraco.providers.csproj @@ -99,6 +99,10 @@ {ccd75ec3-63db-4184-b49d-51c1dd337230} umbraco.cms + + {31785bc3-256c-4613-b2f5-a1b0bdded8c1} + Umbraco.Core + {C7CB79F0-1C97-4B33-BFA7-00731B579AE2} umbraco.datalayer From 98832357bfa6f040dbf0267b2fb4f637aa38b760 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Oct 2013 13:41:06 +1100 Subject: [PATCH 04/10] Lots of work on the member editor - creates new email address prop editor, allows text prop editor to be required based on config, fixes the section directive bug, creating change password prop ed, streamlines more of the services layer to ensure that the things that need to be public are public --- src/Umbraco.Core/Constants-PropertyEditors.cs | 5 + .../PropertyEditors/EmailValidator.cs | 31 +++++ .../Services/ContentTypeService.cs | 31 +---- .../Services/IContentTypeService.cs | 16 +-- .../Services/IMemberTypeService.cs | 13 ++- .../Services/MemberTypeService.cs | 32 +++-- src/Umbraco.Core/Umbraco.Core.csproj | 2 + .../directives/umbsections.directive.js | 1 + .../validation/valcompare.directive.js | 6 +- .../validation/valregex.directive.js | 23 +++- .../src/common/services/util.service.js | 11 +- .../src/views/datatype/edit.html | 3 +- .../changepassword.controller.js | 40 +++++++ .../changepassword/changepassword.html | 33 ++++++ .../propertyeditors/email/email.controller.js | 6 - .../views/propertyeditors/email/email.html | 16 ++- .../propertyeditors/textbox/textbox.html | 11 +- src/Umbraco.Web/Editors/ContentController.cs | 2 +- src/Umbraco.Web/Editors/MemberController.cs | 109 ++++++++++++++---- src/Umbraco.Web/ModelStateExtensions.cs | 27 +++++ .../Models/Mapping/MemberModelMapper.cs | 30 ++++- src/Umbraco.Web/Models/PagedResult.cs | 63 ---------- .../EmailAddressPropertyEditor.cs | 29 +++++ src/Umbraco.Web/Umbraco.Web.csproj | 2 +- .../WebApi/Binders/MemberBinder.cs | 15 ++- .../Filters/ContentItemValidationHelper.cs | 32 +---- 26 files changed, 393 insertions(+), 196 deletions(-) create mode 100644 src/Umbraco.Core/PropertyEditors/EmailValidator.cs create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.html delete mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.controller.js delete mode 100644 src/Umbraco.Web/Models/PagedResult.cs create mode 100644 src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index 3c2a8d6752..e423f58c62 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -411,6 +411,11 @@ namespace Umbraco.Core /// Alias for the XPath DropDownList datatype. /// public const string XPathDropDownListAlias = "Umbraco.XPathDropDownList"; + + /// + /// Alias for the email address property editor + /// + public const string EmailAddressAlias = "Umbraco.EmailAddress"; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/EmailValidator.cs b/src/Umbraco.Core/PropertyEditors/EmailValidator.cs new file mode 100644 index 0000000000..0fb6a227be --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/EmailValidator.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Umbraco.Core.Models; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// A validator that validates an email address + /// + [ValueValidator("Email")] + internal sealed class EmailValidator : ManifestValueValidator, IPropertyValidator + { + public override IEnumerable Validate(object value, string config, PreValueCollection preValues, PropertyEditor editor) + { + var asString = value.ToString(); + + var emailVal = new EmailAddressAttribute(); + + if (emailVal.IsValid(asString) == false) + { + //TODO: localize these! + yield return new ValidationResult("Email is invalid", new[] { "value" }); + } + } + + public IEnumerable Validate(object value, PreValueCollection preValues, PropertyEditor editor) + { + return Validate(value, null, preValues, editor); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index afd7da9efe..a2f27f58a2 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -346,36 +346,7 @@ namespace Umbraco.Core.Services Audit.Add(AuditTypes.Delete, string.Format("Delete ContentTypes performed by user"), userId, -1); } } - - /// - /// Gets an object by its Id - /// - /// Id of the to retrieve - /// - public IMemberType GetMemberType(int id) - { - using (var repository = _repositoryFactory.CreateMemberTypeRepository(_uowProvider.GetUnitOfWork())) - { - return repository.Get(id); - } - } - - /// - /// Gets an object by its Alias - /// - /// Alias of the to retrieve - /// - public IMemberType GetMemberType(string alias) - { - using (var repository = _repositoryFactory.CreateMemberTypeRepository(_uowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Alias == alias); - var contentTypes = repository.GetByQuery(query); - - return contentTypes.FirstOrDefault(); - } - } - + /// /// Gets an object by its Id /// diff --git a/src/Umbraco.Core/Services/IContentTypeService.cs b/src/Umbraco.Core/Services/IContentTypeService.cs index 9f596e465e..9e7c8d53f9 100644 --- a/src/Umbraco.Core/Services/IContentTypeService.cs +++ b/src/Umbraco.Core/Services/IContentTypeService.cs @@ -66,21 +66,7 @@ namespace Umbraco.Core.Services /// Deleting a will delete all the objects based on this /// Optional Id of the User deleting the ContentTypes void Delete(IEnumerable contentTypes, int userId = 0); - - /// - /// Gets an object by its Id - /// - /// Id of the to retrieve - /// - IMemberType GetMemberType(int id); - - /// - /// Gets an object by its Alias - /// - /// Alias of the to retrieve - /// - IMemberType GetMemberType(string alias); - + /// /// Gets an object by its Id /// diff --git a/src/Umbraco.Core/Services/IMemberTypeService.cs b/src/Umbraco.Core/Services/IMemberTypeService.cs index 9c45d83e68..7857e1dda0 100644 --- a/src/Umbraco.Core/Services/IMemberTypeService.cs +++ b/src/Umbraco.Core/Services/IMemberTypeService.cs @@ -12,7 +12,18 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects IEnumerable GetAllMemberTypes(params int[] ids); - IMemberType GetMemberType(string alias); + /// + /// Gets an object by its Id + /// + /// Id of the to retrieve + /// IMemberType GetMemberType(int id); + + /// + /// Gets an object by its Alias + /// + /// Alias of the to retrieve + /// + IMemberType GetMemberType(string alias); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/MemberTypeService.cs b/src/Umbraco.Core/Services/MemberTypeService.cs index cc5eada678..81a231f7c5 100644 --- a/src/Umbraco.Core/Services/MemberTypeService.cs +++ b/src/Umbraco.Core/Services/MemberTypeService.cs @@ -37,17 +37,11 @@ namespace Umbraco.Core.Services } } - public IMemberType GetMemberType(string alias) - { - using (var repository = _repositoryFactory.CreateMemberTypeRepository(_uowProvider.GetUnitOfWork())) - { - var query = Query.Builder.Where(x => x.Alias == alias); - var memberTypes = repository.GetByQuery(query); - - return memberTypes.FirstOrDefault(); - } - } - + /// + /// Gets an object by its Id + /// + /// Id of the to retrieve + /// public IMemberType GetMemberType(int id) { using (var repository = _repositoryFactory.CreateMemberTypeRepository(_uowProvider.GetUnitOfWork())) @@ -56,5 +50,21 @@ namespace Umbraco.Core.Services } } + /// + /// Gets an object by its Alias + /// + /// Alias of the to retrieve + /// + public IMemberType GetMemberType(string alias) + { + using (var repository = _repositoryFactory.CreateMemberTypeRepository(_uowProvider.GetUnitOfWork())) + { + var query = Query.Builder.Where(x => x.Alias == alias); + var contentTypes = repository.GetByQuery(query); + + return contentTypes.FirstOrDefault(); + } + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index e59e6d7303..731f7a0a84 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -358,6 +358,7 @@ + @@ -450,6 +451,7 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/umbsections.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/umbsections.directive.js index 4df4b686c8..f9c83d2d52 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/umbsections.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/umbsections.directive.js @@ -12,6 +12,7 @@ function sectionsDirective($timeout, $window, navigationService, sectionResource scope.maxSections = 7; scope.overflowingSections = 0; + scope.sections = []; function loadSections(){ sectionResource.getSections() diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valcompare.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valcompare.directive.js index 5e7f042825..1a36dcc24f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valcompare.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valcompare.directive.js @@ -1,8 +1,10 @@ angular.module('umbraco.directives.validation') .directive('valCompare',function () { return { - require: "ngModel", - link: function(scope, elem, attrs, ctrl) { + require: "ngModel", + link: function (scope, elem, attrs, ctrl) { + + //TODO: Pretty sure this should be done using a requires ^form in the directive declaration var otherInput = elem.inheritedData("$formController")[attrs.valCompare]; ctrl.$parsers.push(function(value) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js index bf37d73392..d1103cdbc3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js @@ -6,17 +6,36 @@ * NOTE: there's already an ng-pattern but this requires that a regex expression is set, not a regex string **/ function valRegex() { + return { require: 'ngModel', restrict: "A", link: function (scope, elm, attrs, ctrl) { + var flags = ""; + if (attrs.valRegexFlags) { + try { + flags = scope.$eval(attrs.valRegexFlags); + if (!flags) { + flags = attrs.valRegexFlags; + } + } + catch (e) { + flags = attrs.valRegexFlags; + } + } var regex; try { - regex = new RegExp(scope.$eval(attrs.valRegex)); + var resolved = scope.$eval(attrs.valRegex); + if (resolved) { + regex = new RegExp(resolved, flags); + } + else { + regex = new RegExp(attrs.valRegex, flags); + } } catch(e) { - regex = new RegExp(attrs.valRegex); + regex = new RegExp(attrs.valRegex, flags); } var patternValidator = function (viewValue) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js index 1deb464037..a66c3b549e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js @@ -203,7 +203,7 @@ function umbDataFormatter() { /** formats the display model used to display the member to the model used to save the member */ formatMemberPostData: function(displayModel, action) { - //this is basically the same as for media but we need to explicitly add the username,email to the save model + //this is basically the same as for media but we need to explicitly add the username,email, password to the save model var saveModel = this.formatMediaPostData(displayModel, action); var genericTab = _.find(displayModel.tabs, function (item) { @@ -216,8 +216,17 @@ function umbDataFormatter() { var propEmail = _.find(genericTab.properties, function (item) { return item.alias === "_umb_email"; }); + var propPass = _.find(genericTab.properties, function (item) { + return item.alias === "_umb_password"; + }); saveModel.email = propEmail.value; saveModel.username = propLogin.value; + //NOTE: This would only be set for new members! + if (angular.isString(propPass.value)) { + // if we are resetting or changing passwords then that data will come from the property editor and + // it's value will be an object not just a string. + saveModel.password = propPass.value; + } return saveModel; }, diff --git a/src/Umbraco.Web.UI.Client/src/views/datatype/edit.html b/src/Umbraco.Web.UI.Client/src/views/datatype/edit.html index 4a10a5efe1..45a86c7a76 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatype/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatype/edit.html @@ -1,7 +1,8 @@
+ ng-submit="save()" + val-status-changed> diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js new file mode 100644 index 0000000000..9f60d300cd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.controller.js @@ -0,0 +1,40 @@ +angular.module("umbraco").controller("Umbraco.PropertyEditors.ChangePasswordController", + function($scope) { + + //the model config will contain an object, if it does not we'll create defaults + //NOTE: We will not support doing the password regex on the client side because the regex on the server side + //based on the membership provider cannot always be ported to js from .net directly. + /* + { + requiresQuestionAnswer: true/false, + enableReset: true/false, + minPasswordLength: 10 + } + */ + + //set defaults if they are not available + if (!$scope.model.config || !$scope.model.config.requiresQuestionAnswer) { + $scope.model.config.requiresQuestionAnswer = false; + } + if (!$scope.model.config || !$scope.model.config.enableReset) { + $scope.model.config.enableReset = true; + } + if (!$scope.model.config || !$scope.model.config.minPasswordLength) { + $scope.model.config.minPasswordLength = 7; + } + + + $scope.confirm = ""; + + $scope.hasPassword = $scope.model.value !== undefined && $scope.model.value !== null && $scope.model.value !== ""; + + $scope.changing = !$scope.hasPassword; + + $scope.doChange = function() { + $scope.changing = true; + }; + + $scope.cancelChange = function() { + $scope.changing = false; + }; + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.html new file mode 100644 index 0000000000..c624a81cc2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/changepassword/changepassword.html @@ -0,0 +1,33 @@ +
+
+ +
+ Password changing or resetting is currently not supported +
+
+
+
+ + + + Required + Minimum {{model.config.minPasswordLength}} characters + + +
+
+ + + + Passwords must match + +
+ +
+
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.controller.js deleted file mode 100644 index 3c44b970b8..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.controller.js +++ /dev/null @@ -1,6 +0,0 @@ -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.EmailController", - function($rootScope, $scope, dialogService, $routeParams, contentResource, contentTypeResource, editorContextService, notificationsService) { - - - }); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html index c33db4d337..635717be99 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/email/email.html @@ -1,3 +1,13 @@ - -Invalid email - \ No newline at end of file +
+ + + + Required + Invalid email + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html index b17c59dea7..377f8e3db7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html @@ -1,2 +1,9 @@ - - \ No newline at end of file +
+ + + Required + +
diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 2875eb446d..83fcc44aba 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -255,7 +255,7 @@ namespace Umbraco.Web.Editors else { //publish the item and check if it worked, if not we will show a diff msg below - publishStatus = ((ContentService)Services.ContentService).SaveAndPublishInternal(contentItem.PersistedContent, (int)Security.CurrentUser.Id); + publishStatus = Services.ContentService.SaveAndPublishWithStatus(contentItem.PersistedContent, (int)Security.CurrentUser.Id); } diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index ae30a4d618..65f45677c9 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -1,15 +1,19 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; using System.Web.Http.ModelBinding; using AutoMapper; using Examine.LuceneEngine.SearchCriteria; using Examine.SearchCriteria; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Web.WebApi; using Umbraco.Web.Models.ContentEditing; @@ -77,9 +81,8 @@ namespace Umbraco.Web.Editors /// Gets an empty content item for the ///
/// - /// /// - public MemberDisplay GetEmpty(string contentTypeAlias, string username, string password) + public MemberDisplay GetEmpty(string contentTypeAlias) { var contentType = Services.MemberTypeService.GetMemberType(contentTypeAlias); if (contentType == null) @@ -87,7 +90,7 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } - var emptyContent = new Core.Models.Member("", "", "", "", -1, contentType); + var emptyContent = new Core.Models.Member("", contentType); return Mapper.Map(emptyContent); } @@ -96,6 +99,7 @@ namespace Umbraco.Web.Editors ///
/// [FileUploadCleanupFilter] + [MembershipProviderValidationFilter] public MemberDisplay PostSave( [ModelBinder(typeof(MemberBinder))] MemberSave contentItem) @@ -109,30 +113,18 @@ namespace Umbraco.Web.Editors UpdateName(contentItem); - //map the custom properties + //map the custom properties - this will already be set for new entities in our member binder contentItem.PersistedContent.Email = contentItem.Email; - //TODO: If we allow changing the alias then we'll need to change URLs, etc... in the editor, would prefer to use - // a unique id but then need to figure out how to handle that with custom membership providers - waiting on feedback from morten. - + contentItem.PersistedContent.Username = contentItem.Username; + MapPropertyValues(contentItem); - //We need to manually check the validation results here because: - // * We still need to save the entity even if there are validation value errors - // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) - // then we cannot continue saving, we can only display errors - // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display - // a message indicating this + //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) - { - if (ValidationHelper.ModelHasRequiredForPersistenceErrors(contentItem) - && (contentItem.Action == ContentSaveAction.SaveNew)) - { - //ok, so the absolute mandatory data is invalid and it's new, we cannot actually continue! - // add the modelstate to the outgoing object and throw validation response - var forDisplay = Mapper.Map(contentItem.PersistedContent); - forDisplay.Errors = ModelState.ToErrorDictionary(); - throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); - } + { + var forDisplay = Mapper.Map(contentItem.PersistedContent); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); } //save the item @@ -175,4 +167,75 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } } + + /// + /// This validates the submitted data in regards to the current membership provider + /// + internal class MembershipProviderValidationFilterAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(HttpActionContext actionContext) + { + base.OnActionExecuting(actionContext); + + var membershipProvider = Membership.Providers[Constants.Conventions.Member.UmbracoMemberProviderName]; + if (membershipProvider == null) + { + throw new InvalidOperationException("No membership provider found with name " + Constants.Conventions.Member.UmbracoMemberProviderName); + } + + var contentItem = (MemberSave) actionContext.ActionArguments["contentItem"]; + + var validEmail = ValidateUniqueEmail(contentItem, membershipProvider, actionContext); + if (validEmail == false) + { + actionContext.ModelState.AddPropertyError(new ValidationResult("Email address is already in use"), "umb_email"); + } + } + + internal bool ValidateUniqueEmail(MemberSave contentItem, MembershipProvider membershipProvider, HttpActionContext actionContext) + { + if (contentItem == null) throw new ArgumentNullException("contentItem"); + if (membershipProvider == null) throw new ArgumentNullException("membershipProvider"); + + if (membershipProvider.RequiresUniqueEmail == false) + { + return true; + } + + int totalRecs; + var existingByEmail = membershipProvider.FindUsersByEmail(contentItem.Email.Trim(), 0, int.MaxValue, out totalRecs); + switch (contentItem.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 (contentItem.PersistedContent.Email.InvariantEquals(contentItem.Email.Trim()) == false) + { + //they are changing their email + if (existingByEmail.Cast().Select(x => x.Email) + .Any(x => x.InvariantEquals(contentItem.Email.Trim()))) + { + //the user cannot use this email + return false; + } + } + break; + case ContentSaveAction.SaveNew: + //check if the user's email already exists + if (existingByEmail.Cast().Select(x => x.Email) + .Any(x => x.InvariantEquals(contentItem.Email.Trim()))) + { + //the user cannot use this email + return false; + } + break; + case ContentSaveAction.Publish: + case ContentSaveAction.PublishNew: + default: + //we don't support this for members + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + return true; + } + } } diff --git a/src/Umbraco.Web/ModelStateExtensions.cs b/src/Umbraco.Web/ModelStateExtensions.cs index 964b7712b4..b656f87e5a 100644 --- a/src/Umbraco.Web/ModelStateExtensions.cs +++ b/src/Umbraco.Web/ModelStateExtensions.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Web.Mvc; @@ -48,6 +49,32 @@ namespace Umbraco.Web // state.AddModelError("DataValidation", errorMessage); //} + /// + /// Adds the error to model state correctly for a property so we can use it on the client side. + /// + /// + /// + /// + internal static void AddPropertyError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, ValidationResult result, string propertyAlias) + { + //if there are no member names supplied then we assume that the validation message is for the overall property + // not a sub field on the property editor + if (!result.MemberNames.Any()) + { + //add a model state error for the entire property + modelState.AddModelError(string.Format("{0}.{1}", "Properties", propertyAlias), result.ErrorMessage); + } + else + { + //there's assigned field names so we'll combine the field name with the property name + // so that we can try to match it up to a real sub field of this editor + foreach (var field in result.MemberNames) + { + modelState.AddModelError(string.Format("{0}.{1}.{2}", "Properties", propertyAlias, field), result.ErrorMessage); + } + } + } + public static IDictionary ToErrorDictionary(this System.Web.Http.ModelBinding.ModelStateDictionary modelState) { var modelStateError = new Dictionary(); diff --git a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs index 001296792f..37690321c9 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs @@ -1,4 +1,5 @@ -using AutoMapper; +using System.Collections.Generic; +using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Mapping; @@ -53,7 +54,9 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap>() .ForMember( dto => dto.Owner, - expression => expression.ResolveUsing>()); + expression => expression.ResolveUsing>()) + //do no map the custom member properties (currently anyways, they were never there in 6.x) + .ForMember(dto => dto.Properties, expression => expression.ResolveUsing()); } /// @@ -70,7 +73,8 @@ namespace Umbraco.Web.Models.Mapping Alias = string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix), Label = ui.Text("login"), Value = display.Username, - View = "textbox" + View = "textbox", + Config = new Dictionary { { "IsRequired", true } } }, new ContentPropertyDisplay { @@ -84,10 +88,28 @@ namespace Umbraco.Web.Models.Mapping Alias = string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix), Label = ui.Text("general", "email"), Value = display.Email, - View = "textbox" + View = "email", + Config = new Dictionary {{"IsRequired", true}} }); } + /// + /// This ensures that the custom membership provider properties are not mapped (currently since they weren't there in v6) + /// + /// + /// Because these properties don't exist on the form, if we don't remove them for this map we'll get validation errors when posting data + /// + internal class MemberDtoPropertiesValueResolver : ValueResolver> + { + protected override IEnumerable ResolveCore(IMember source) + { + var exclude = Constants.Conventions.Member.StandardPropertyTypeStubs.Select(x => x.Value.Alias).ToArray(); + return source.Properties + .Where(x => exclude.Contains(x.Alias) == false) + .Select(Mapper.Map); + } + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/PagedResult.cs b/src/Umbraco.Web/Models/PagedResult.cs deleted file mode 100644 index 6c2c764c6e..0000000000 --- a/src/Umbraco.Web/Models/PagedResult.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.Serialization; - -namespace Umbraco.Web.Models -{ - /// - /// Represents a paged result for a model collection - /// - /// - [DataContract(Name = "pagedCollection", Namespace = "")] - public class PagedResult - { - public PagedResult(long totalItems, long pageNumber, long pageSize) - { - TotalItems = totalItems; - PageNumber = pageNumber; - PageSize = pageSize; - - if (pageSize > 0) - { - TotalPages = (long) Math.Ceiling(totalItems/(Decimal) pageSize); - } - else - { - TotalPages = 1; - } - } - - [DataMember(Name = "pageNumber")] - public long PageNumber { get; private set; } - - [DataMember(Name = "pageSize")] - public long PageSize { get; private set; } - - [DataMember(Name = "totalPages")] - public long TotalPages { get; private set; } - - [DataMember(Name = "totalItems")] - public long TotalItems { get; private set; } - - [DataMember(Name = "items")] - public IEnumerable Items { get; set; } - - /// - /// Calculates the skip size based on the paged parameters specified - /// - /// - /// Returns 0 if the page number or page size is zero - /// - internal int SkipSize - { - get - { - if (PageNumber > 0 && PageSize > 0) - { - return Convert.ToInt32((PageNumber - 1)*PageSize); - } - return 0; - } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs new file mode 100644 index 0000000000..565928435a --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/EmailAddressPropertyEditor.cs @@ -0,0 +1,29 @@ +using Umbraco.Core; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + [PropertyEditor(Constants.PropertyEditors.EmailAddressAlias, "Email address", "email")] + public class EmailAddressPropertyEditor : PropertyEditor + { + protected override PropertyValueEditor CreateValueEditor() + { + var editor = base.CreateValueEditor(); + //add an email address validator + editor.Validators.Add(new EmailValidator()); + return editor; + } + + protected override PreValueEditor CreatePreValueEditor() + { + return new EmailAddressePreValueEditor(); + } + + internal class EmailAddressePreValueEditor : PreValueEditor + { + [PreValueField("Required?", "boolean")] + public bool IsRequired { get; set; } + } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index dc7ace1be7..9b989fb336 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -327,6 +327,7 @@ + @@ -351,7 +352,6 @@ - diff --git a/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs b/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs index 0051bf94d8..aabfdcb7b9 100644 --- a/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/MemberBinder.cs @@ -25,7 +25,8 @@ namespace Umbraco.Web.WebApi.Binders protected override IMember GetExisting(MemberSave model) { - //TODO: We're going to remove the built-in member properties from this editor - not sure if we should be persisting them elsewhere ? + //TODO: We're going to remove the built-in member properties from this editor - We didn't support these in 6.x so + // pretty hard to support them in 7 when the member type editor is using the old APIs! var toRemove = Constants.Conventions.Member.StandardPropertyTypeStubs.Select(x => x.Value.Alias).ToArray(); var member = ApplicationContext.Services.MemberService.GetByUsername(model.Username); @@ -38,11 +39,21 @@ namespace Umbraco.Web.WebApi.Binders protected override IMember CreateNew(MemberSave model) { - var contentType = ApplicationContext.Services.ContentTypeService.GetMemberType(model.ContentTypeAlias); + var contentType = ApplicationContext.Services.MemberTypeService.GetMemberType(model.ContentTypeAlias); if (contentType == null) { throw new InvalidOperationException("No member type found wth alias " + model.ContentTypeAlias); } + + //TODO: We're going to remove the built-in member properties from this editor - We didn't support these in 6.x so + // pretty hard to support them in 7 when the member type editor is using the old APIs! + var toRemove = Constants.Conventions.Member.StandardPropertyTypeStubs.Select(x => x.Value.Alias).ToArray(); + foreach (var remove in toRemove) + { + contentType.RemovePropertyType(remove); + } + + //return the new member with the details filled in return new Member(model.Name, model.Email, model.Username, model.Password, -1, contentType); } diff --git a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs b/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs index a68eb2cbbf..7875bfb076 100644 --- a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs +++ b/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs @@ -139,7 +139,7 @@ namespace Umbraco.Web.WebApi.Filters foreach (var result in editor.ValueEditor.Validators.SelectMany(v => v.Validate(postedValue, preValues, editor))) { - AddError(actionContext.ModelState, result, p.Alias); + actionContext.ModelState.AddPropertyError(result, p.Alias); } //Now we need to validate the property based on the PropertyType validation (i.e. regex and required) @@ -148,7 +148,7 @@ namespace Umbraco.Web.WebApi.Filters { foreach (var result in p.PropertyEditor.ValueEditor.RequiredValidator.Validate(postedValue, "", preValues, editor)) { - AddError(actionContext.ModelState, result, p.Alias); + actionContext.ModelState.AddPropertyError(result, p.Alias); } } @@ -156,7 +156,7 @@ namespace Umbraco.Web.WebApi.Filters { foreach (var result in p.PropertyEditor.ValueEditor.RegexValidator.Validate(postedValue, p.ValidationRegExp, preValues, editor)) { - AddError(actionContext.ModelState, result, p.Alias); + actionContext.ModelState.AddPropertyError(result, p.Alias); } } } @@ -164,30 +164,6 @@ namespace Umbraco.Web.WebApi.Filters return actionContext.ModelState.IsValid; } - /// - /// Adds the error to model state correctly for a property so we can use it on the client side. - /// - /// - /// - /// - private void AddError(ModelStateDictionary modelState, ValidationResult result, string propertyAlias) - { - //if there are no member names supplied then we assume that the validation message is for the overall property - // not a sub field on the property editor - if (!result.MemberNames.Any()) - { - //add a model state error for the entire property - modelState.AddModelError(string.Format("{0}.{1}", "Properties", propertyAlias), result.ErrorMessage); - } - else - { - //there's assigned field names so we'll combine the field name with the property name - // so that we can try to match it up to a real sub field of this editor - foreach (var field in result.MemberNames) - { - modelState.AddModelError(string.Format("{0}.{1}.{2}", "Properties", propertyAlias, field), result.ErrorMessage); - } - } - } + } } \ No newline at end of file From 7919a39bb680f0353619ce46aecdd13575938974 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Oct 2013 14:10:04 +1100 Subject: [PATCH 05/10] updates UmbracoMembershipMember --- .../Membership/UmbracoMembershipMember.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/Umbraco.Core/Models/Membership/UmbracoMembershipMember.cs b/src/Umbraco.Core/Models/Membership/UmbracoMembershipMember.cs index 06e50ecd9b..d55cc77f66 100644 --- a/src/Umbraco.Core/Models/Membership/UmbracoMembershipMember.cs +++ b/src/Umbraco.Core/Models/Membership/UmbracoMembershipMember.cs @@ -2,6 +2,8 @@ namespace Umbraco.Core.Models.Membership { + //TODO: THere's still a bunch of properties that don't exist in this use that need to be mapped somehow. + internal class UmbracoMembershipMember : MembershipUser { private readonly IMember _member; @@ -21,5 +23,44 @@ namespace Umbraco.Core.Models.Membership get { return _member.Email; } set { _member.Email = value; } } + + public override object ProviderUserKey + { + get { return _member.Key; } + } + + public override System.DateTime CreationDate + { + get { return _member.CreateDate; } + } + + public override string UserName + { + get { return _member.Username; } + } + + public override string Comment + { + get { return _member.Comments; } + set { _member.Comments = value; } + } + + public override bool IsApproved + { + get { return _member.IsApproved; } + set { _member.IsApproved = value; } + } + + public override bool IsLockedOut + { + get { return _member.IsLockedOut; } + } + + public override System.DateTime LastLoginDate + { + get { return _member.LastLoginDate; } + set { _member.LastLoginDate = value; } + } + } } \ No newline at end of file From 49110ffaf79ba6498d9617dfb871230a77ffa795 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Oct 2013 14:13:21 +1100 Subject: [PATCH 06/10] Changes the legacy membership provider to use the custom member converter which works like the legacy one used to - the new extension method isn't quite there yet. --- .../members/MembersMembershipProvider.cs | 72 +++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/src/umbraco.providers/members/MembersMembershipProvider.cs b/src/umbraco.providers/members/MembersMembershipProvider.cs index 7c8fe8e63a..41e4a4bbcc 100644 --- a/src/umbraco.providers/members/MembersMembershipProvider.cs +++ b/src/umbraco.providers/members/MembersMembershipProvider.cs @@ -18,8 +18,6 @@ using System.Security.Permissions; using System.Runtime.CompilerServices; using Member = umbraco.cms.businesslogic.member.Member; using MemberType = umbraco.cms.businesslogic.member.MemberType; -using Umbraco.Core.Models.Membership; -using User = umbraco.BusinessLogic.User; #endregion @@ -452,7 +450,7 @@ namespace umbraco.providers.members var collection = new MembershipUserCollection(); foreach (var m in byEmail.Skip(pagedResult.SkipSize).Take(pageSize)) { - collection.Add(m.AsConcreteMembershipUser()); + collection.Add(ConvertToMembershipUser(m)); } return collection; } @@ -766,7 +764,27 @@ namespace umbraco.providers.members return null; } - + + private static string GetMemberProperty(IMember m, string propertyAlias, bool isBool) + { + if (!String.IsNullOrEmpty(propertyAlias)) + { + if (m.Properties[propertyAlias] != null && + m.Properties[propertyAlias].Value != null) + { + if (isBool) + { + // Umbraco stored true as 1, which means it can be bool.tryParse'd + return m.Properties[propertyAlias].Value.ToString().Replace("1", "true").Replace("0", "false"); + } + else + return m.Properties[propertyAlias].Value.ToString(); + } + } + + return null; + } + /// /// Verifies that the specified user name and password exist in the data source. /// @@ -989,6 +1007,52 @@ namespace umbraco.providers.members } } + /// + /// Converts to membership user. + /// + /// The m. + /// + private MembershipUser ConvertToMembershipUser(IMember m) + { + if (m == null) return null; + else + { + DateTime lastLogin = DateTime.Now; + bool isApproved = true; + bool isLocked = false; + string comment = ""; + string passwordQuestion = ""; + + // last login + if (!String.IsNullOrEmpty(m_LastLoginPropertyTypeAlias)) + { + DateTime.TryParse(GetMemberProperty(m, m_LastLoginPropertyTypeAlias, false), out lastLogin); + } + // approved + if (!String.IsNullOrEmpty(m_ApprovedPropertyTypeAlias)) + { + bool.TryParse(GetMemberProperty(m, m_ApprovedPropertyTypeAlias, true), out isApproved); + } + // locked + if (!String.IsNullOrEmpty(m_LockPropertyTypeAlias)) + { + bool.TryParse(GetMemberProperty(m, m_LockPropertyTypeAlias, true), out isLocked); + } + // comment + if (!String.IsNullOrEmpty(m_CommentPropertyTypeAlias)) + { + comment = GetMemberProperty(m, m_CommentPropertyTypeAlias, false); + } + // password question + if (!String.IsNullOrEmpty(m_PasswordRetrievalQuestionPropertyTypeAlias)) + { + passwordQuestion = GetMemberProperty(m, m_PasswordRetrievalQuestionPropertyTypeAlias, false); + } + + return new MembershipUser(m_providerName, m.Username, m.Id, m.Email, passwordQuestion, comment, isApproved, isLocked, m.CreateDate, lastLogin, + DateTime.Now, DateTime.Now, DateTime.Now); + } + } #endregion } } From 8917a5ad92f087ce5c55e20a9b945c7940747e57 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Oct 2013 14:50:25 +1100 Subject: [PATCH 07/10] More member service and provider updates --- .../Interfaces/IMemberRepository.cs | 1 - .../Repositories/MemberRepository.cs | 22 ------------------- src/Umbraco.Core/Services/IMemberService.cs | 2 +- src/Umbraco.Core/Services/MemberService.cs | 17 +++++++++++--- .../Providers/MembersMembershipProvider.cs | 2 +- .../members/MembersMembershipProvider.cs | 2 +- 6 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs index 92a8b5365b..895124f363 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs @@ -12,6 +12,5 @@ namespace Umbraco.Core.Persistence.Repositories /// IEnumerable GetByMemberGroup(string groupName); - IEnumerable GetMembersByEmails(params string[] emails); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index c9d716746e..dbaec8471a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -437,28 +437,6 @@ namespace Umbraco.Core.Persistence.Repositories return BuildFromDtos(dtos); } - public IEnumerable GetMembersByEmails(params string[] emails) - { - var sql = GetBaseQuery(false); - if (emails.Any()) - { - var statement = string.Join(" OR ", - emails.Select(x => - string.Format( - "cmsMember.Email='{0}'", - //we have to escape the @ symbol for petapoco to work!! with 2 @@ symbols - Database.EscapeAtSymbols(x)))); - sql.Where(statement); - } - sql.OrderByDescending(x => x.VersionDate); - - var dtos = - Database.Fetch( - new PropertyDataRelator().Map, sql); - - return BuildFromDtos(dtos); - } - private IMember BuildFromDto(List dtos) { if (dtos == null || dtos.Any() == false) diff --git a/src/Umbraco.Core/Services/IMemberService.cs b/src/Umbraco.Core/Services/IMemberService.cs index 004a66d151..c01a80fb9f 100644 --- a/src/Umbraco.Core/Services/IMemberService.cs +++ b/src/Umbraco.Core/Services/IMemberService.cs @@ -38,6 +38,6 @@ namespace Umbraco.Core.Services void Save(IMember membershipUser); - IEnumerable GetMembersByEmails(params string[] emails); + IEnumerable FindMembersByEmail(string emailStringToMatch); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 21cce31400..87eee84863 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -108,11 +108,22 @@ namespace Umbraco.Core.Services } } - public IEnumerable GetMembersByEmails(params string[] emails) + /// + /// Does a search for members that contain the specified string in their email address + /// + /// + /// + public IEnumerable FindMembersByEmail(string emailStringToMatch) { - using (var repository = _repositoryFactory.CreateMemberRepository(_uowProvider.GetUnitOfWork())) + var uow = _uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateMemberRepository(uow)) { - return repository.GetMembersByEmails(emails); + var query = new Query(); + + + query.Where(member => member.Email.Contains(emailStringToMatch)); + + return repository.GetByQuery(query); } } diff --git a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs index 0cc4146f85..1f83d7a686 100644 --- a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs @@ -689,7 +689,7 @@ namespace Umbraco.Web.Security.Providers /// public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords) { - var byEmail = MemberService.GetMembersByEmails(emailToMatch).ToArray(); + var byEmail = MemberService.FindMembersByEmail(emailToMatch).ToArray(); totalRecords = byEmail.Length; var pagedResult = new PagedResult(totalRecords, pageIndex, pageSize); diff --git a/src/umbraco.providers/members/MembersMembershipProvider.cs b/src/umbraco.providers/members/MembersMembershipProvider.cs index 41e4a4bbcc..011be286ad 100644 --- a/src/umbraco.providers/members/MembersMembershipProvider.cs +++ b/src/umbraco.providers/members/MembersMembershipProvider.cs @@ -443,7 +443,7 @@ namespace umbraco.providers.members /// public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords) { - var byEmail = ApplicationContext.Current.Services.MemberService.GetMembersByEmails(emailToMatch).ToArray(); + var byEmail = ApplicationContext.Current.Services.MemberService.FindMembersByEmail(emailToMatch).ToArray(); totalRecords = byEmail.Length; var pagedResult = new PagedResult(totalRecords, pageIndex, pageSize); From 8ad4ee4e585a372567d9a20af1bb02db2cfb77f0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Oct 2013 16:48:02 +1100 Subject: [PATCH 08/10] Fixes/updates more of the member repository and related items --- src/Umbraco.Core/Models/Member.cs | 2 +- .../Repositories/MemberRepository.cs | 12 ++++++++-- .../Providers/MembersMembershipProvider.cs | 22 ++++++++++--------- .../businesslogic/member/Member.cs | 2 ++ .../members/MembersMembershipProvider.cs | 8 ++++++- 5 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs index 650f664254..3530e94c59 100644 --- a/src/Umbraco.Core/Models/Member.cs +++ b/src/Umbraco.Core/Models/Member.cs @@ -438,7 +438,7 @@ namespace Umbraco.Core.Models public override void ChangeTrashedState(bool isTrashed, int parentId = -20) { - throw new NotImplementedException("Members can't be trashed as no Recycle Bin exists, so use of this method is invalid"); + throw new NotSupportedException("Members can't be trashed as no Recycle Bin exists, so use of this method is invalid"); } /* Internal experiment - only used for mapping queries. diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index dbaec8471a..6175c2d494 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -329,11 +329,19 @@ namespace Umbraco.Core.Persistence.Repositories //TODO ContentType for the Member entity //Create the PropertyData for this version - cmsPropertyData - var propertyFactory = new PropertyFactory(entity.ContentType, entity.Version, entity.Id); - var propertyDataDtos = propertyFactory.BuildDto(((Member)entity).Properties); + var propertyFactory = new PropertyFactory(entity.ContentType, entity.Version, entity.Id); var keyDictionary = new Dictionary(); //Add Properties + // - don't try to save the property if it doesn't exist (or doesn't have an ID) on the content type + // - this can occur if the member type doesn't contain the built-in properties that the + // - member object contains. + var existingProperties = entity.Properties + .Where(property => entity.ContentType.PropertyTypes.Any(x => x.Alias == property.Alias && x.HasIdentity)) + .ToList(); + + var propertyDataDtos = propertyFactory.BuildDto(existingProperties); + foreach (var propertyDataDto in propertyDataDtos) { if (propertyDataDto.Id > 0) diff --git a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs index 1f83d7a686..c4b493c166 100644 --- a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Specialized; +using System.ComponentModel.DataAnnotations; using System.Configuration.Provider; using System.Linq; using System.Security.Cryptography; @@ -332,6 +333,9 @@ namespace Umbraco.Web.Security.Providers member.PasswordQuestion = passwordQuestion; member.PasswordAnswer = passwordAnswer; + //encrypts/hashes the password depending on the settings + member.Password = EncryptOrHashPassword(member.Password); + MemberService.Save(member); status = MembershipCreateStatus.Success; @@ -362,7 +366,7 @@ namespace Umbraco.Web.Security.Providers } var member = MemberService.GetByUsername(username); - var encodedPassword = EncodePassword(password); + var encodedPassword = EncryptOrHashPassword(password); if (member.Password == encodedPassword) { @@ -429,12 +433,12 @@ namespace Umbraco.Web.Security.Providers var member = MemberService.GetByUsername(username); if (member == null) return false; - var encodedPassword = EncodePassword(oldPassword); + var encodedPassword = EncryptOrHashPassword(oldPassword); if (member.Password == encodedPassword) { - member.Password = EncodePassword(newPassword); + member.Password = EncryptOrHashPassword(newPassword); MemberService.Save(member); return true; @@ -466,7 +470,7 @@ namespace Umbraco.Web.Security.Providers if (_requiresQuestionAndAnswer == false || (_requiresQuestionAndAnswer && answer == member.PasswordAnswer)) { member.Password = - EncodePassword(Membership.GeneratePassword(_minRequiredPasswordLength, + EncryptOrHashPassword(Membership.GeneratePassword(_minRequiredPasswordLength, _minRequiredNonAlphanumericCharacters)); MemberService.Save(member); } @@ -507,7 +511,7 @@ namespace Umbraco.Web.Security.Providers if (member.IsLockedOut) throw new ProviderException("The member is locked out."); - var encodedPassword = EncodePassword(password); + var encodedPassword = EncryptOrHashPassword(password); var authenticated = (encodedPassword == member.Password); @@ -725,11 +729,9 @@ namespace Umbraco.Web.Security.Providers private bool IsEmaiValid(string email) { - const string pattern = @"^(?!\.)(""([^""\r\\]|\\[""\r\\])*""|" - + @"([-a-z0-9!#$%&'*+/=?^_`{|}~]|(? @@ -737,7 +739,7 @@ namespace Umbraco.Web.Security.Providers /// /// The password. /// The encoded password. - private string EncodePassword(string password) + private string EncryptOrHashPassword(string password) { var encodedPassword = password; switch (PasswordFormat) diff --git a/src/umbraco.cms/businesslogic/member/Member.cs b/src/umbraco.cms/businesslogic/member/Member.cs index 3394144096..5578ba2691 100644 --- a/src/umbraco.cms/businesslogic/member/Member.cs +++ b/src/umbraco.cms/businesslogic/member/Member.cs @@ -212,6 +212,8 @@ namespace umbraco.cms.businesslogic.member /// The new member public static Member MakeNew(string Name, string LoginName, string Email, MemberType mbt, User u) { + if (mbt == null) throw new ArgumentNullException("mbt"); + var loginName = (!String.IsNullOrEmpty(LoginName)) ? LoginName : Name; if (String.IsNullOrEmpty(loginName)) diff --git a/src/umbraco.providers/members/MembersMembershipProvider.cs b/src/umbraco.providers/members/MembersMembershipProvider.cs index 011be286ad..cf3121268c 100644 --- a/src/umbraco.providers/members/MembersMembershipProvider.cs +++ b/src/umbraco.providers/members/MembersMembershipProvider.cs @@ -381,7 +381,13 @@ namespace umbraco.providers.members status = MembershipCreateStatus.DuplicateEmail; else { - Member m = Member.MakeNew(username, email, MemberType.GetByAlias(m_DefaultMemberTypeAlias), User.GetUser(0)); + var memberType = MemberType.GetByAlias(m_DefaultMemberTypeAlias); + if (memberType == null) + { + throw new InvalidOperationException("Could not find a member type with alias " + m_DefaultMemberTypeAlias + ". Ensure your membership provider configuration is up to date and that the default member type exists."); + } + + Member m = Member.MakeNew(username, email, memberType, User.GetUser(0)); m.Password = password; MembershipUser mUser = From 5df30ac0cce8b3cdad8bcde4fb142cc8c322240c Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Oct 2013 17:22:57 +1100 Subject: [PATCH 09/10] tweaks to membership providers so if we cast we can specify the member type. --- .../Providers/MembersMembershipProvider.cs | 37 +++++-- .../members/MembersMembershipProvider.cs | 104 +++++++++++------- 2 files changed, 92 insertions(+), 49 deletions(-) diff --git a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs index c4b493c166..811fc6be52 100644 --- a/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs +++ b/src/Umbraco.Web/Security/Providers/MembersMembershipProvider.cs @@ -257,8 +257,9 @@ namespace Umbraco.Web.Security.Providers } /// - /// Adds a new membership user to the data source. + /// Adds a new membership user to the data source with the specified member type /// + /// A specific member type to create the member for /// The user name for the new user. /// The password for the new user. /// The e-mail address for the new user. @@ -270,16 +271,16 @@ namespace Umbraco.Web.Security.Providers /// /// A object populated with the information for the newly created user. /// - public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, + public MembershipUser CreateUser(string memberType, string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status) { - LogHelper.Debug("Member signup requested: username -> " + username + ". email -> " +email); + LogHelper.Debug("Member signup requested: username -> " + username + ". email -> " + email); // Validate password if (IsPasswordValid(password) == false) { - status = MembershipCreateStatus.InvalidPassword; - return null; + status = MembershipCreateStatus.InvalidPassword; + return null; } // Validate email @@ -327,21 +328,41 @@ namespace Umbraco.Web.Security.Providers return null; } - var member = MemberService.CreateMember(email, username, password, DefaultMemberTypeAlias); + var member = MemberService.CreateMember(email, username, password, memberType); member.IsApproved = isApproved; member.PasswordQuestion = passwordQuestion; member.PasswordAnswer = passwordAnswer; - + //encrypts/hashes the password depending on the settings member.Password = EncryptOrHashPassword(member.Password); MemberService.Save(member); - + status = MembershipCreateStatus.Success; return member.AsConcreteMembershipUser(); } + /// + /// Adds a new membership user to the data source. + /// + /// The user name for the new user. + /// The password for the new user. + /// The e-mail address for the new user. + /// The password question for the new user. + /// The password answer for the new user + /// Whether or not the new user is approved to be validated. + /// The unique identifier from the membership data source for the user. + /// A enumeration value indicating whether the user was created successfully. + /// + /// A object populated with the information for the newly created user. + /// + public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, + bool isApproved, object providerUserKey, out MembershipCreateStatus status) + { + return CreateUser(DefaultMemberTypeAlias, username, password, email, passwordQuestion, passwordAnswer, isApproved, providerUserKey, out status); + } + /// /// Processes a request to update the password question and answer for a membership user. /// diff --git a/src/umbraco.providers/members/MembersMembershipProvider.cs b/src/umbraco.providers/members/MembersMembershipProvider.cs index cf3121268c..2e906d7f8d 100644 --- a/src/umbraco.providers/members/MembersMembershipProvider.cs +++ b/src/umbraco.providers/members/MembersMembershipProvider.cs @@ -358,6 +358,68 @@ namespace umbraco.providers.members } } + /// + /// Adds a new membership user to the data source. + /// + /// + /// The user name for the new user. + /// The password for the new user. + /// The e-mail address for the new user. + /// The password question for the new user. + /// The password answer for the new user + /// Whether or not the new user is approved to be validated. + /// The unique identifier from the membership data source for the user. + /// A enumeration value indicating whether the user was created successfully. + /// + /// A object populated with the information for the newly created user. + /// + public MembershipUser CreateUser(string memberTypeAlias, string username, string password, string email, string passwordQuestion, + string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status) + { + if (Member.GetMemberFromLoginName(username) != null) + status = MembershipCreateStatus.DuplicateUserName; + else if (Member.GetMemberFromEmail(email) != null && RequiresUniqueEmail) + status = MembershipCreateStatus.DuplicateEmail; + else + { + var memberType = MemberType.GetByAlias(memberTypeAlias); + if (memberType == null) + { + throw new InvalidOperationException("Could not find a member type with alias " + memberTypeAlias + ". Ensure your membership provider configuration is up to date and that the default member type exists."); + } + + Member m = Member.MakeNew(username, email, memberType, User.GetUser(0)); + m.Password = password; + + MembershipUser mUser = + ConvertToMembershipUser(m); + + // custom fields + if (!String.IsNullOrEmpty(m_PasswordRetrievalQuestionPropertyTypeAlias)) + UpdateMemberProperty(m, m_PasswordRetrievalQuestionPropertyTypeAlias, passwordQuestion); + + if (!String.IsNullOrEmpty(m_PasswordRetrievalAnswerPropertyTypeAlias)) + UpdateMemberProperty(m, m_PasswordRetrievalAnswerPropertyTypeAlias, passwordAnswer); + + if (!String.IsNullOrEmpty(m_ApprovedPropertyTypeAlias)) + UpdateMemberProperty(m, m_ApprovedPropertyTypeAlias, isApproved); + + if (!String.IsNullOrEmpty(m_LastLoginPropertyTypeAlias)) + { + mUser.LastActivityDate = DateTime.Now; + UpdateMemberProperty(m, m_LastLoginPropertyTypeAlias, mUser.LastActivityDate); + } + + // save + m.Save(); + + status = MembershipCreateStatus.Success; + + return mUser; + } + return null; + } + /// /// Adds a new membership user to the data source. /// @@ -375,47 +437,7 @@ namespace umbraco.providers.members public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status) { - if (Member.GetMemberFromLoginName(username) != null) - status = MembershipCreateStatus.DuplicateUserName; - else if (Member.GetMemberFromEmail(email) != null && RequiresUniqueEmail) - status = MembershipCreateStatus.DuplicateEmail; - else - { - var memberType = MemberType.GetByAlias(m_DefaultMemberTypeAlias); - if (memberType == null) - { - throw new InvalidOperationException("Could not find a member type with alias " + m_DefaultMemberTypeAlias + ". Ensure your membership provider configuration is up to date and that the default member type exists."); - } - - Member m = Member.MakeNew(username, email, memberType, User.GetUser(0)); - m.Password = password; - - MembershipUser mUser = - ConvertToMembershipUser(m); - - // custom fields - if (!String.IsNullOrEmpty(m_PasswordRetrievalQuestionPropertyTypeAlias)) - UpdateMemberProperty(m, m_PasswordRetrievalQuestionPropertyTypeAlias, passwordQuestion); - - if (!String.IsNullOrEmpty(m_PasswordRetrievalAnswerPropertyTypeAlias)) - UpdateMemberProperty(m, m_PasswordRetrievalAnswerPropertyTypeAlias, passwordAnswer); - - if (!String.IsNullOrEmpty(m_ApprovedPropertyTypeAlias)) - UpdateMemberProperty(m, m_ApprovedPropertyTypeAlias, isApproved); - - if (!String.IsNullOrEmpty(m_LastLoginPropertyTypeAlias)) { - mUser.LastActivityDate = DateTime.Now; - UpdateMemberProperty(m, m_LastLoginPropertyTypeAlias, mUser.LastActivityDate); - } - - // save - m.Save(); - - status = MembershipCreateStatus.Success; - - return mUser; - } - return null; + return CreateUser(m_DefaultMemberTypeAlias, username, password, email, passwordQuestion, passwordAnswer, isApproved, providerUserKey, out status); } /// From 52aac5ba106c649ef81a0af57757397b389db05e Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 10 Oct 2013 17:44:56 +1100 Subject: [PATCH 10/10] Ok, got members creating so long as the umbraco membership provider is being used - otherwise currenlty you'll get a ysod saying we don't support that. Now to get updating working. --- .../validation/valpropertymsg.directive.js | 4 +- .../services/contenteditinghelper.service.js | 2 +- .../views/member/member.edit.controller.js | 2 + src/Umbraco.Web/Editors/ContentController.cs | 61 +++-- .../Editors/ContentControllerBase.cs | 7 +- src/Umbraco.Web/Editors/MediaController.cs | 14 +- src/Umbraco.Web/Editors/MemberController.cs | 227 +++++++++++------- ...ershipProviderValidationFilterAttribute.cs | 132 ++++++++++ .../Models/ContentEditing/MemberDisplay.cs | 9 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 10 files changed, 344 insertions(+), 115 deletions(-) create mode 100644 src/Umbraco.Web/Editors/MembershipProviderValidationFilterAttribute.cs diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index cd5a2e5dbf..ad1b268618 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -102,7 +102,9 @@ function valPropertyMsg(serverValidationManager) { var errCount = 0; for (var e in formCtrl.$error) { - errCount++; + if (e) { + errCount++; + } } if ((errCount === 1 && formCtrl.$error.valPropertyMsg !== undefined) || diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index 03fd6b2946..25faf0b75c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -213,7 +213,7 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser } args.scope.$broadcast("saved", { scope: args.scope }); - if (!this.redirectToCreatedContent(args.newContent.id)) { + if (!this.redirectToCreatedContent(args.redirectId ? args.redirectId : args.newContent.id)) { //we are not redirecting because this is not new content, it is existing content. In this case // we need to detect what properties have changed and re-bind them with the server data. diff --git a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js index df2b122ed0..43500e10e3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js @@ -65,6 +65,8 @@ function MemberEditController($scope, $routeParams, $q, $timeout, $window, membe contentEditingHelper.handleSuccessfulSave({ scope: $scope, newContent: data, + //specify a custom id to redirect to since we want to use the GUID + redirectId: data.key, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) }); diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index ea164921a9..8d067b9643 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -185,32 +185,7 @@ 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 - - UpdateName(contentItem); - - //TODO: We need to support 'send to publish' - - contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate; - contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; - //only set the template if it didn't change - var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) - || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); - if (templateChanged) - { - var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); - if (template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) - { - //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); - LogHelper.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias); - } - else - { - //NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template - contentItem.PersistedContent.Template = template; - } - } - + MapPropertyValues(contentItem); //We need to manually check the validation results here because: @@ -281,6 +256,40 @@ namespace Umbraco.Web.Editors return display; } + /// + /// Maps the dto property values to the persisted model + /// + /// + private void MapPropertyValues(ContentItemSave contentItem) + { + UpdateName(contentItem); + + //TODO: We need to support 'send to publish' + + contentItem.PersistedContent.ExpireDate = contentItem.ExpireDate; + contentItem.PersistedContent.ReleaseDate = contentItem.ReleaseDate; + //only set the template if it didn't change + var templateChanged = (contentItem.PersistedContent.Template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) + || (contentItem.PersistedContent.Template != null && contentItem.PersistedContent.Template.Alias != contentItem.TemplateAlias) + || (contentItem.PersistedContent.Template != null && contentItem.TemplateAlias.IsNullOrWhiteSpace()); + if (templateChanged) + { + var template = Services.FileService.GetTemplate(contentItem.TemplateAlias); + if (template == null && contentItem.TemplateAlias.IsNullOrWhiteSpace() == false) + { + //ModelState.AddModelError("Template", "No template exists with the specified alias: " + contentItem.TemplateAlias); + LogHelper.Warn("No template exists with the specified alias: " + contentItem.TemplateAlias); + } + else + { + //NOTE: this could be null if there was a template and the posted template is null, this should remove the assigned template + contentItem.PersistedContent.Template = template; + } + } + + base.MapPropertyValues(contentItem); + } + /// /// Publishes a document with a given ID /// diff --git a/src/Umbraco.Web/Editors/ContentControllerBase.cs b/src/Umbraco.Web/Editors/ContentControllerBase.cs index e7508f0b12..14a15b7415 100644 --- a/src/Umbraco.Web/Editors/ContentControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentControllerBase.cs @@ -78,7 +78,12 @@ namespace Umbraco.Web.Editors return null; } - protected void MapPropertyValues(ContentBaseItemSave contentItem) + /// + /// Maps the dto property values to the persisted model + /// + /// + /// + protected virtual void MapPropertyValues(ContentBaseItemSave contentItem) where TPersisted : IContentBase { //Map the property values diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 728510e75a..d2aca4f1c5 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -287,8 +287,6 @@ namespace Umbraco.Web.Editors // * we have a reference to the DTO object and the persisted object // * Permissions are valid - UpdateName(contentItem); - MapPropertyValues(contentItem); //We need to manually check the validation results here because: @@ -331,6 +329,18 @@ namespace Umbraco.Web.Editors return display; } + /// + /// Maps the property values to the persisted entity + /// + /// + protected override void MapPropertyValues(ContentBaseItemSave contentItem) + { + UpdateName(contentItem); + + //use the base method to map the rest of the properties + base.MapPropertyValues(contentItem); + } + /// /// Change the sort order for media /// diff --git a/src/Umbraco.Web/Editors/MemberController.cs b/src/Umbraco.Web/Editors/MemberController.cs index 65f45677c9..a17817ced4 100644 --- a/src/Umbraco.Web/Editors/MemberController.cs +++ b/src/Umbraco.Web/Editors/MemberController.cs @@ -1,20 +1,18 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; -using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Threading.Tasks; using System.Web.Http; -using System.Web.Http.Controllers; -using System.Web.Http.Filters; using System.Web.Http.ModelBinding; +using System.Web.Security; using AutoMapper; using Examine.LuceneEngine.SearchCriteria; using Examine.SearchCriteria; -using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; using Umbraco.Web.WebApi; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; @@ -23,7 +21,6 @@ using Umbraco.Web.WebApi.Filters; using umbraco; using Constants = Umbraco.Core.Constants; using Examine; -using System.Web.Security; using Member = umbraco.cms.businesslogic.member.Member; namespace Umbraco.Web.Editors @@ -104,6 +101,11 @@ namespace Umbraco.Web.Editors [ModelBinder(typeof(MemberBinder))] MemberSave contentItem) { + 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 // * and validated @@ -111,23 +113,53 @@ namespace Umbraco.Web.Editors // * we have a reference to the DTO object and the persisted object // * Permissions are valid - 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; - + //map the properties to the persisted entity MapPropertyValues(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) + if (ModelState.IsValid == false) { var forDisplay = Mapper.Map(contentItem.PersistedContent); forDisplay.Errors = ModelState.ToErrorDictionary(); throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); } + //TODO: WE need to support this! - requires UI updates, etc... + if (Membership.Provider.RequiresQuestionAndAnswer) + { + throw new NotSupportedException("Currently the member editor does not support providers that have RequiresQuestionAndAnswer specified"); + } + + //Depending on the action we need to first do a create or update using the membership provider + // this ensures that passwords are formatted correclty and also performs the validation on the provider itself. + switch (contentItem.Action) + { + case ContentSaveAction.Save: + //TODO: Update with the provider! - change password, etc... + break; + case ContentSaveAction.SaveNew: + MembershipCreateStatus status; + CreateWithUmbracoProvider(contentItem, out status); + break; + default: + //we don't support anything else for members + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + //If we've had problems creating/updating the user with the provider then return the error + if (ModelState.IsValid == false) + { + var forDisplay = Mapper.Map(contentItem.PersistedContent); + forDisplay.Errors = ModelState.ToErrorDictionary(); + 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); //return the updated model @@ -148,6 +180,106 @@ namespace Umbraco.Web.Editors 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.MapPropertyValues(contentItem); + } + + /// + /// This is going to create the user with the membership provider and check for validation + /// + /// + /// + /// + /// + /// 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) + { + //if we are creating a new one, create the member using the membership provider first + + //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! + + //NOTE: We are casting directly to the umbraco membership provider so we can specify the member type that we want to use! + + var umbracoMembershipProvider = (global::umbraco.providers.members.UmbracoMembershipProvider)Membership.Provider; + var membershipUser = umbracoMembershipProvider.CreateUser( + contentItem.ContentTypeAlias, contentItem.Username, contentItem.Password, contentItem.Email, "", "", true, Guid.NewGuid(), out status); + + //TODO: Localize these! + switch (status) + { + case MembershipCreateStatus.Success: + + //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: + ModelState.AddPropertyError( + new ValidationResult("Invalid user name", new[] { "value" }), + string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + break; + case MembershipCreateStatus.InvalidPassword: + ModelState.AddPropertyError( + new ValidationResult("Invalid password", new[] { "value" }), + string.Format("{0}password", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + break; + case MembershipCreateStatus.InvalidQuestion: + case MembershipCreateStatus.InvalidAnswer: + throw new NotSupportedException("Currently the member editor does not support providers that have RequiresQuestionAndAnswer specified"); + case MembershipCreateStatus.InvalidEmail: + ModelState.AddPropertyError( + new ValidationResult("Invalid email", new[] { "value" }), + string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + break; + case MembershipCreateStatus.DuplicateUserName: + ModelState.AddPropertyError( + new ValidationResult("Username is already in use", new[] { "value" }), + string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + break; + case MembershipCreateStatus.DuplicateEmail: + ModelState.AddPropertyError( + new ValidationResult("Email address is already in use", new[] { "value" }), + string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + break; + case MembershipCreateStatus.InvalidProviderUserKey: + ModelState.AddPropertyError( + //specify 'default' just so that it shows up as a notification - is not assigned to a property + new ValidationResult("Invalid provider user key"), "default"); + break; + case MembershipCreateStatus.DuplicateProviderUserKey: + ModelState.AddPropertyError( + //specify 'default' just so that it shows up as a notification - is not assigned to a property + new ValidationResult("Duplicate provider user key"), "default"); + break; + case MembershipCreateStatus.ProviderError: + case MembershipCreateStatus.UserRejected: + ModelState.AddPropertyError( + //specify 'default' just so that it shows up as a notification - is not assigned to a property + new ValidationResult("User could not be created (rejected by provider)"), "default"); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + return membershipUser; + } /// /// Permanently deletes a member @@ -167,75 +299,4 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } } - - /// - /// This validates the submitted data in regards to the current membership provider - /// - internal class MembershipProviderValidationFilterAttribute : ActionFilterAttribute - { - public override void OnActionExecuting(HttpActionContext actionContext) - { - base.OnActionExecuting(actionContext); - - var membershipProvider = Membership.Providers[Constants.Conventions.Member.UmbracoMemberProviderName]; - if (membershipProvider == null) - { - throw new InvalidOperationException("No membership provider found with name " + Constants.Conventions.Member.UmbracoMemberProviderName); - } - - var contentItem = (MemberSave) actionContext.ActionArguments["contentItem"]; - - var validEmail = ValidateUniqueEmail(contentItem, membershipProvider, actionContext); - if (validEmail == false) - { - actionContext.ModelState.AddPropertyError(new ValidationResult("Email address is already in use"), "umb_email"); - } - } - - internal bool ValidateUniqueEmail(MemberSave contentItem, MembershipProvider membershipProvider, HttpActionContext actionContext) - { - if (contentItem == null) throw new ArgumentNullException("contentItem"); - if (membershipProvider == null) throw new ArgumentNullException("membershipProvider"); - - if (membershipProvider.RequiresUniqueEmail == false) - { - return true; - } - - int totalRecs; - var existingByEmail = membershipProvider.FindUsersByEmail(contentItem.Email.Trim(), 0, int.MaxValue, out totalRecs); - switch (contentItem.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 (contentItem.PersistedContent.Email.InvariantEquals(contentItem.Email.Trim()) == false) - { - //they are changing their email - if (existingByEmail.Cast().Select(x => x.Email) - .Any(x => x.InvariantEquals(contentItem.Email.Trim()))) - { - //the user cannot use this email - return false; - } - } - break; - case ContentSaveAction.SaveNew: - //check if the user's email already exists - if (existingByEmail.Cast().Select(x => x.Email) - .Any(x => x.InvariantEquals(contentItem.Email.Trim()))) - { - //the user cannot use this email - return false; - } - break; - case ContentSaveAction.Publish: - case ContentSaveAction.PublishNew: - default: - //we don't support this for members - throw new HttpResponseException(HttpStatusCode.NotFound); - } - - return true; - } - } } diff --git a/src/Umbraco.Web/Editors/MembershipProviderValidationFilterAttribute.cs b/src/Umbraco.Web/Editors/MembershipProviderValidationFilterAttribute.cs new file mode 100644 index 0000000000..8fbd226f0f --- /dev/null +++ b/src/Umbraco.Web/Editors/MembershipProviderValidationFilterAttribute.cs @@ -0,0 +1,132 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Net; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; +using System.Web.Security; +using Umbraco.Core; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Editors +{ + /// + /// This validates the submitted data in regards to the current membership provider + /// + internal class MembershipProviderValidationFilterAttribute : ActionFilterAttribute + { + public override void OnActionExecuting(HttpActionContext actionContext) + { + base.OnActionExecuting(actionContext); + + //default provider! + var membershipProvider = Membership.Provider; + + var contentItem = (MemberSave) actionContext.ActionArguments["contentItem"]; + + var validEmail = ValidateUniqueEmail(contentItem, membershipProvider, actionContext); + if (validEmail == false) + { + actionContext.ModelState.AddPropertyError( + new ValidationResult("Email address is already in use", new[] {"value"}), + string.Format("{0}email", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + } + + var validLogin = ValidateUniqueLogin(contentItem, membershipProvider, actionContext); + if (validLogin == false) + { + actionContext.ModelState.AddPropertyError( + new ValidationResult("Username is already in use", new[] { "value" }), + string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + } + } + + internal bool ValidateUniqueLogin(MemberSave contentItem, MembershipProvider membershipProvider, HttpActionContext actionContext) + { + if (contentItem == null) throw new ArgumentNullException("contentItem"); + if (membershipProvider == null) throw new ArgumentNullException("membershipProvider"); + + int totalRecs; + var existingByEmail = membershipProvider.FindUsersByName(contentItem.Username.Trim(), 0, int.MaxValue, out totalRecs); + switch (contentItem.Action) + { + case ContentSaveAction.Save: + + if (contentItem.PersistedContent.Email.InvariantEquals(contentItem.Email.Trim()) == false) + { + //they are changing their login name + if (existingByEmail.Cast().Select(x => x.UserName) + .Any(x => x.InvariantEquals(contentItem.Username.Trim()))) + { + //the user cannot use this login + return false; + } + } + break; + case ContentSaveAction.SaveNew: + //check if the user's login already exists + if (existingByEmail.Cast().Select(x => x.UserName) + .Any(x => x.InvariantEquals(contentItem.Username.Trim()))) + { + //the user cannot use this login + return false; + } + break; + case ContentSaveAction.Publish: + case ContentSaveAction.PublishNew: + default: + //we don't support this for members + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + return true; + } + + internal bool ValidateUniqueEmail(MemberSave contentItem, MembershipProvider membershipProvider, HttpActionContext actionContext) + { + if (contentItem == null) throw new ArgumentNullException("contentItem"); + if (membershipProvider == null) throw new ArgumentNullException("membershipProvider"); + + if (membershipProvider.RequiresUniqueEmail == false) + { + return true; + } + + int totalRecs; + var existingByEmail = membershipProvider.FindUsersByEmail(contentItem.Email.Trim(), 0, int.MaxValue, out totalRecs); + switch (contentItem.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 (contentItem.PersistedContent.Email.InvariantEquals(contentItem.Email.Trim()) == false) + { + //they are changing their email + if (existingByEmail.Cast().Select(x => x.Email) + .Any(x => x.InvariantEquals(contentItem.Email.Trim()))) + { + //the user cannot use this email + return false; + } + } + break; + case ContentSaveAction.SaveNew: + //check if the user's email already exists + if (existingByEmail.Cast().Select(x => x.Email) + .Any(x => x.InvariantEquals(contentItem.Email.Trim()))) + { + //the user cannot use this email + return false; + } + break; + case ContentSaveAction.Publish: + case ContentSaveAction.PublishNew: + default: + //we don't support this for members + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs index 27a4eaca0f..48c6a43322 100644 --- a/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/MemberDisplay.cs @@ -1,4 +1,5 @@ -using System.Runtime.Serialization; +using System; +using System.Runtime.Serialization; using Umbraco.Core.Models; namespace Umbraco.Web.Models.ContentEditing @@ -16,5 +17,11 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "email")] public string Email { get; set; } + /// + /// This is the unique Id stored in the database - but could also be the unique id for a custom membership provider + /// + [DataMember(Name = "key")] + public Guid Key { get; set; } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index b1892c1b3b..dbf529f4ce 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -309,6 +309,7 @@ +