using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; using Umbraco.Core.Composing; using Umbraco.Core.Events; using Umbraco.Core.Exceptions; using Umbraco.Core.Models; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Scoping; namespace Umbraco.Core.Services.Implement { /// /// Represents the MemberService. /// public class MemberService : ScopeRepositoryService, IMemberService { private readonly IMemberRepository _memberRepository; private readonly IMemberTypeRepository _memberTypeRepository; private readonly IMemberGroupRepository _memberGroupRepository; private readonly IAuditRepository _auditRepository; private readonly IMemberTypeService _memberTypeService; private readonly IMemberGroupService _memberGroupService; #region Constructor public MemberService(IScopeProvider provider, ILoggerFactory loggerFactory, IEventMessagesFactory eventMessagesFactory, IMemberGroupService memberGroupService, IMemberRepository memberRepository, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, IAuditRepository auditRepository) : base(provider, loggerFactory, eventMessagesFactory) { _memberRepository = memberRepository; _memberTypeRepository = memberTypeRepository; _memberGroupRepository = memberGroupRepository; _auditRepository = auditRepository; _memberGroupService = memberGroupService ?? throw new ArgumentNullException(nameof(memberGroupService)); } #endregion #region Count /// /// Gets the total number of Members based on the count type /// /// /// The way the Online count is done is the same way that it is done in the MS SqlMembershipProvider - We query for any members /// that have their last active date within the Membership.UserIsOnlineTimeWindow (which is in minutes). It isn't exact science /// but that is how MS have made theirs so we'll follow that principal. /// /// to count by /// with number of Members for passed in type public int GetCount(MemberCountType countType) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); IQuery query; switch (countType) { case MemberCountType.All: query = Query(); break; case MemberCountType.LockedOut: query = Query().Where(x => x.PropertyTypeAlias == Constants.Conventions.Member.IsLockedOut && ((Member) x).BoolPropertyValue); break; case MemberCountType.Approved: query = Query().Where(x => x.PropertyTypeAlias == Constants.Conventions.Member.IsApproved && ((Member) x).BoolPropertyValue); break; default: throw new ArgumentOutOfRangeException(nameof(countType)); } return _memberRepository.GetCountByQuery(query); } } /// /// Gets the count of Members by an optional MemberType alias /// /// If no alias is supplied then the count for all Member will be returned /// Optional alias for the MemberType when counting number of Members /// with number of Members public int Count(string memberTypeAlias = null) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); return _memberRepository.Count(memberTypeAlias); } } #endregion #region Create /// /// Creates an object without persisting it /// /// This method is convenient for when you need to add properties to a new Member /// before persisting it in order to limit the amount of times its saved. /// Also note that the returned will not have an Id until its saved. /// Username of the Member to create /// Email of the Member to create /// Name of the Member to create /// Alias of the MemberType the Member should be based on /// public IMember CreateMember(string username, string email, string name, string memberTypeAlias) { var memberType = GetMemberType(memberTypeAlias); if (memberType == null) throw new ArgumentException("No member type with that alias.", nameof(memberTypeAlias)); var member = new Member(name, email.ToLower().Trim(), username, memberType); using (var scope = ScopeProvider.CreateScope()) { CreateMember(scope, member, 0, false); scope.Complete(); } return member; } /// /// Creates an object without persisting it /// /// This method is convenient for when you need to add properties to a new Member /// before persisting it in order to limit the amount of times its saved. /// Also note that the returned will not have an Id until its saved. /// Username of the Member to create /// Email of the Member to create /// Name of the Member to create /// MemberType the Member should be based on /// public IMember CreateMember(string username, string email, string name, IMemberType memberType) { if (memberType == null) throw new ArgumentNullException(nameof(memberType)); var member = new Member(name, email.ToLower().Trim(), username, memberType); using (var scope = ScopeProvider.CreateScope()) { CreateMember(scope, member, 0, false); scope.Complete(); } return member; } /// /// Creates and persists a new /// /// An can be of type or /// Username of the to create /// Email of the to create /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database /// Alias of the Type /// Is the member approved /// IMember IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias) { return CreateMemberWithIdentity(username, email, username, passwordValue, memberTypeAlias); } /// /// Creates and persists a new /// /// An can be of type or /// Username of the to create /// Email of the to create /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database /// Alias of the Type /// /// IMember IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias, bool isApproved) { return CreateMemberWithIdentity(username, email, username, passwordValue, memberTypeAlias, isApproved); } public IMember CreateMemberWithIdentity(string username, string email, string memberTypeAlias) { return CreateMemberWithIdentity(username, email, username, "", memberTypeAlias); } public IMember CreateMemberWithIdentity(string username, string email, string memberTypeAlias, bool isApproved) { return CreateMemberWithIdentity(username, email, username, "", memberTypeAlias, isApproved); } public IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias) { return CreateMemberWithIdentity(username, email, name, "", memberTypeAlias); } public IMember CreateMemberWithIdentity(string username, string email, string name, string memberTypeAlias, bool isApproved) { return CreateMemberWithIdentity(username, email, name, "", memberTypeAlias, isApproved); } /// /// Creates and persists a Member /// /// Using this method will persist the Member object before its returned /// meaning that it will have an Id available (unlike the CreateMember method) /// Username of the Member to create /// Email of the Member to create /// Name of the Member to create /// Alias of the MemberType the Member should be based on /// Optional IsApproved of the Member to create /// public IMember CreateMemberWithIdentity(string username, string email, string name, string passwordValue, string memberTypeAlias, bool isApproved = true) { using (var scope = ScopeProvider.CreateScope()) { // locking the member tree secures member types too scope.WriteLock(Constants.Locks.MemberTree); var memberType = GetMemberType(scope, memberTypeAlias); // + locks // + locks if (memberType == null) throw new ArgumentException("No member type with that alias.", nameof(memberTypeAlias)); // causes rollback // causes rollback var member = new Member(name, email.ToLower().Trim(), username, passwordValue, memberType, isApproved); CreateMember(scope, member, -1, true); scope.Complete(); return member; } } public IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType) { return CreateMemberWithIdentity(username, email, username, "", memberType); } /// /// Creates and persists a Member /// /// Using this method will persist the Member object before its returned /// meaning that it will have an Id available (unlike the CreateMember method) /// Username of the Member to create /// Email of the Member to create /// MemberType the Member should be based on /// public IMember CreateMemberWithIdentity(string username, string email, IMemberType memberType, bool isApproved) { return CreateMemberWithIdentity(username, email, username, "", memberType, isApproved); } public IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType) { return CreateMemberWithIdentity(username, email, name, "", memberType); } /// /// Creates and persists a Member /// /// Using this method will persist the Member object before its returned /// meaning that it will have an Id available (unlike the CreateMember method) /// Username of the Member to create /// Email of the Member to create /// Name of the Member to create /// MemberType the Member should be based on /// public IMember CreateMemberWithIdentity(string username, string email, string name, IMemberType memberType, bool isApproved) { return CreateMemberWithIdentity(username, email, name, "", memberType, isApproved); } /// /// Creates and persists a Member /// /// Using this method will persist the Member object before its returned /// meaning that it will have an Id available (unlike the CreateMember method) /// Username of the Member to create /// Email of the Member to create /// Name of the Member to create /// This value should be the encoded/encrypted/hashed value for the password that will be stored in the database /// MemberType the Member should be based on /// private IMember CreateMemberWithIdentity(string username, string email, string name, string passwordValue, IMemberType memberType, bool isApproved = true) { if (memberType == null) throw new ArgumentNullException(nameof(memberType)); using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MemberTree); // ensure it all still make sense // ensure it all still make sense var vrfy = GetMemberType(scope, memberType.Alias); // + locks if (vrfy == null || vrfy.Id != memberType.Id) throw new ArgumentException($"Member type with alias {memberType.Alias} does not exist or is a different member type."); // causes rollback var member = new Member(name, email.ToLower().Trim(), username, passwordValue, memberType, isApproved); CreateMember(scope, member, -1, true); scope.Complete(); return member; } } private void CreateMember(IScope scope, Member member, int userId, bool withIdentity) { member.CreatorId = userId; if (withIdentity) { // if saving is cancelled, media remains without an identity var saveEventArgs = new SaveEventArgs(member); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) return; _memberRepository.Save(member); saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs); } if (withIdentity == false) return; Audit(AuditType.New, member.CreatorId, member.Id, $"Member '{member.Name}' was created with Id {member.Id}"); } #endregion #region Get, Has, Is, Exists... /// /// Gets a Member by its integer id /// /// Id /// public IMember GetById(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); return _memberRepository.Get(id); } } /// /// Gets a Member by the unique key /// /// The guid key corresponds to the unique id in the database /// and the user id in the membership provider. /// Id /// public IMember GetByKey(Guid id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); var query = Query().Where(x => x.Key == id); return _memberRepository.Get(query).FirstOrDefault(); } } /// /// Gets a list of paged objects /// /// Current page index /// Size of the page /// Total number of records found (out) /// public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); return _memberRepository.GetPage(null, pageIndex, pageSize, out totalRecords, null, Ordering.By("LoginName")); } } public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, string memberTypeAlias = null, string filter = "") { return GetAll(pageIndex, pageSize, out totalRecords, orderBy, orderDirection, true, memberTypeAlias, filter); } public IEnumerable GetAll(long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, bool orderBySystemField, string memberTypeAlias, string filter) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); var query1 = memberTypeAlias == null ? null : Query().Where(x => x.ContentTypeAlias == memberTypeAlias); var query2 = filter == null ? null : Query().Where(x => x.Name.Contains(filter) || x.Username.Contains(filter) || x.Email.Contains(filter)); return _memberRepository.GetPage(query1, pageIndex, pageSize, out totalRecords, query2, Ordering.By(orderBy, orderDirection, isCustomField: !orderBySystemField)); } } /// /// Gets an by its provider key /// /// Id to use for retrieval /// public IMember GetByProviderKey(object id) { var asGuid = id.TryConvertTo(); if (asGuid.Success) return GetByKey(asGuid.Result); var asInt = id.TryConvertTo(); if (asInt.Success) return GetById(asInt.Result); return null; } /// /// Get an by email /// /// Email to use for retrieval /// public IMember GetByEmail(string email) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); var query = Query().Where(x => x.Email.Equals(email)); return _memberRepository.Get(query).FirstOrDefault(); } } /// /// Get an by username /// /// Username to use for retrieval /// public IMember GetByUsername(string username) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); return _memberRepository.GetByUsername(username); } } /// /// Gets all Members for the specified MemberType alias /// /// Alias of the MemberType /// public IEnumerable GetMembersByMemberType(string memberTypeAlias) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); var query = Query().Where(x => x.ContentTypeAlias == memberTypeAlias); return _memberRepository.Get(query); } } /// /// Gets all Members for the MemberType id /// /// Id of the MemberType /// public IEnumerable GetMembersByMemberType(int memberTypeId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); var query = Query().Where(x => x.ContentTypeId == memberTypeId); return _memberRepository.Get(query); } } /// /// Gets all Members within the specified MemberGroup name /// /// Name of the MemberGroup /// public IEnumerable GetMembersByGroup(string memberGroupName) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); return _memberRepository.GetByMemberGroup(memberGroupName); } } /// /// Gets all Members with the ids specified /// /// If no Ids are specified all Members will be retrieved /// Optional list of Member Ids /// public IEnumerable GetAllMembers(params int[] ids) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); return _memberRepository.GetMany(ids); } } /// /// Finds Members based on their display name /// /// Display name to match /// Current page index /// Size of the page /// Total number of records found (out) /// The type of match to make as . Default is /// public IEnumerable FindMembersByDisplayName(string displayNameToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); var query = Query(); switch (matchType) { case StringPropertyMatchType.Exact: query.Where(member => member.Name.Equals(displayNameToMatch)); break; case StringPropertyMatchType.Contains: query.Where(member => member.Name.Contains(displayNameToMatch)); break; case StringPropertyMatchType.StartsWith: query.Where(member => member.Name.StartsWith(displayNameToMatch)); break; case StringPropertyMatchType.EndsWith: query.Where(member => member.Name.EndsWith(displayNameToMatch)); break; case StringPropertyMatchType.Wildcard: query.Where(member => member.Name.SqlWildcard(displayNameToMatch, TextColumnType.NVarchar)); break; default: throw new ArgumentOutOfRangeException(nameof(matchType)); // causes rollback // causes rollback } return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("Name")); } } /// /// Finds a list of objects by a partial email string /// /// Partial email string to match /// Current page index /// Size of the page /// Total number of records found (out) /// The type of match to make as . Default is /// public IEnumerable FindByEmail(string emailStringToMatch, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); var query = Query(); switch (matchType) { case StringPropertyMatchType.Exact: query.Where(member => member.Email.Equals(emailStringToMatch)); break; case StringPropertyMatchType.Contains: query.Where(member => member.Email.Contains(emailStringToMatch)); break; case StringPropertyMatchType.StartsWith: query.Where(member => member.Email.StartsWith(emailStringToMatch)); break; case StringPropertyMatchType.EndsWith: query.Where(member => member.Email.EndsWith(emailStringToMatch)); break; case StringPropertyMatchType.Wildcard: query.Where(member => member.Email.SqlWildcard(emailStringToMatch, TextColumnType.NVarchar)); break; default: throw new ArgumentOutOfRangeException(nameof(matchType)); } return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("Email")); } } /// /// Finds a list of objects by a partial username /// /// Partial username to match /// Current page index /// Size of the page /// Total number of records found (out) /// The type of match to make as . Default is /// public IEnumerable FindByUsername(string login, long pageIndex, int pageSize, out long totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); var query = Query(); switch (matchType) { case StringPropertyMatchType.Exact: query.Where(member => member.Username.Equals(login)); break; case StringPropertyMatchType.Contains: query.Where(member => member.Username.Contains(login)); break; case StringPropertyMatchType.StartsWith: query.Where(member => member.Username.StartsWith(login)); break; case StringPropertyMatchType.EndsWith: query.Where(member => member.Username.EndsWith(login)); break; case StringPropertyMatchType.Wildcard: query.Where(member => member.Email.SqlWildcard(login, TextColumnType.NVarchar)); break; default: throw new ArgumentOutOfRangeException(nameof(matchType)); } return _memberRepository.GetPage(query, pageIndex, pageSize, out totalRecords, null, Ordering.By("LoginName")); } } /// /// Gets a list of Members based on a property search /// /// Alias of the PropertyType to search for /// Value to match /// The type of match to make as . Default is /// public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, string value, StringPropertyMatchType matchType = StringPropertyMatchType.Exact) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); IQuery query; switch (matchType) { case StringPropertyMatchType.Exact: query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue.SqlEquals(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue.SqlEquals(value, TextColumnType.NVarchar))); break; case StringPropertyMatchType.Contains: query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue.SqlContains(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue.SqlContains(value, TextColumnType.NVarchar))); break; case StringPropertyMatchType.StartsWith: query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue.SqlStartsWith(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue.SqlStartsWith(value, TextColumnType.NVarchar))); break; case StringPropertyMatchType.EndsWith: query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && (((Member) x).LongStringPropertyValue.SqlEndsWith(value, TextColumnType.NText) || ((Member) x).ShortStringPropertyValue.SqlEndsWith(value, TextColumnType.NVarchar))); break; default: throw new ArgumentOutOfRangeException(nameof(matchType)); } return _memberRepository.Get(query); } } /// /// Gets a list of Members based on a property search /// /// Alias of the PropertyType to search for /// Value to match /// The type of match to make as . Default is /// public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, int value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); IQuery query; switch (matchType) { case ValuePropertyMatchType.Exact: query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue == value); break; case ValuePropertyMatchType.GreaterThan: query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue > value); break; case ValuePropertyMatchType.LessThan: query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue < value); break; case ValuePropertyMatchType.GreaterThanOrEqualTo: query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue >= value); break; case ValuePropertyMatchType.LessThanOrEqualTo: query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).IntegerPropertyValue <= value); break; default: throw new ArgumentOutOfRangeException(nameof(matchType)); } return _memberRepository.Get(query); } } /// /// Gets a list of Members based on a property search /// /// Alias of the PropertyType to search for /// Value to match /// public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, bool value) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); var query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).BoolPropertyValue == value); return _memberRepository.Get(query); } } /// /// Gets a list of Members based on a property search /// /// Alias of the PropertyType to search for /// Value to match /// The type of match to make as . Default is /// public IEnumerable GetMembersByPropertyValue(string propertyTypeAlias, DateTime value, ValuePropertyMatchType matchType = ValuePropertyMatchType.Exact) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); IQuery query; switch (matchType) { case ValuePropertyMatchType.Exact: query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue == value); break; case ValuePropertyMatchType.GreaterThan: query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue > value); break; case ValuePropertyMatchType.LessThan: query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue < value); break; case ValuePropertyMatchType.GreaterThanOrEqualTo: query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue >= value); break; case ValuePropertyMatchType.LessThanOrEqualTo: query = Query().Where(x => ((Member) x).PropertyTypeAlias == propertyTypeAlias && ((Member) x).DateTimePropertyValue <= value); break; default: throw new ArgumentOutOfRangeException(nameof(matchType)); // causes rollback // causes rollback } // TODO: Since this is by property value, we need a GetByPropertyQuery on the repo! return _memberRepository.Get(query); } } /// /// Checks if a Member with the id exists /// /// Id of the Member /// True if the Member exists otherwise False public bool Exists(int id) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); return _memberRepository.Exists(id); } } /// /// Checks if a Member with the username exists /// /// Username to check /// True if the Member exists otherwise False public bool Exists(string username) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); return _memberRepository.Exists(username); } } #endregion #region Save /// public void SetLastLogin(string username, DateTime date) { using (var scope = ScopeProvider.CreateScope()) { _memberRepository.SetLastLogin(username, date); scope.Complete(); } } /// public void Save(IMember member, bool raiseEvents = true) { //trimming username and email to make sure we have no trailing space member.Username = member.Username.Trim(); member.Email = member.Email.Trim(); using (var scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(member); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) { scope.Complete(); return; } if (string.IsNullOrWhiteSpace(member.Name)) { throw new ArgumentException("Cannot save member with empty name."); } scope.WriteLock(Constants.Locks.MemberTree); _memberRepository.Save(member); if (raiseEvents) { saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs); } Audit(AuditType.Save, 0, member.Id); scope.Complete(); } } /// public void Save(IEnumerable members, bool raiseEvents = true) { var membersA = members.ToArray(); using (var scope = ScopeProvider.CreateScope()) { var saveEventArgs = new SaveEventArgs(membersA); if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs)) { scope.Complete(); return; } scope.WriteLock(Constants.Locks.MemberTree); foreach (var member in membersA) { //trimming username and email to make sure we have no trailing space member.Username = member.Username.Trim(); member.Email = member.Email.Trim(); _memberRepository.Save(member); } if (raiseEvents) { saveEventArgs.CanCancel = false; scope.Events.Dispatch(Saved, this, saveEventArgs); } Audit(AuditType.Save, 0, -1, "Save multiple Members"); scope.Complete(); } } #endregion #region Delete /// /// Deletes an /// /// to Delete public void Delete(IMember member) { using (var scope = ScopeProvider.CreateScope()) { var deleteEventArgs = new DeleteEventArgs(member); if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) { scope.Complete(); return; } scope.WriteLock(Constants.Locks.MemberTree); DeleteLocked(scope, member, deleteEventArgs); Audit(AuditType.Delete, 0, member.Id); scope.Complete(); } } private void DeleteLocked(IScope scope, IMember member, DeleteEventArgs args = null) { // a member has no descendants _memberRepository.Delete(member); if (args == null) args = new DeleteEventArgs(member, false); // raise event & get flagged files else args.CanCancel = false; scope.Events.Dispatch(Deleted, this, args); // media files deleted by QueuingEventDispatcher } #endregion #region Roles public void AddRole(string roleName) { using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MemberTree); _memberGroupRepository.CreateIfNotExists(roleName); scope.Complete(); } } public IEnumerable GetAllRoles() { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); return _memberGroupRepository.GetMany().Select(x => x.Name).Distinct(); } } public IEnumerable GetAllRoles(int memberId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); var result = _memberGroupRepository.GetMemberGroupsForMember(memberId); return result.Select(x => x.Name).Distinct(); } } public IEnumerable GetAllRoles(string username) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); var result = _memberGroupRepository.GetMemberGroupsForMember(username); return result.Select(x => x.Name).Distinct(); } } public IEnumerable GetAllRolesIds() { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); return _memberGroupRepository.GetMany().Select(x => x.Id).Distinct(); } } public IEnumerable GetAllRolesIds(int memberId) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); var result = _memberGroupRepository.GetMemberGroupsForMember(memberId); return result.Select(x => x.Id).Distinct(); } } public IEnumerable GetAllRolesIds(string username) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); var result = _memberGroupRepository.GetMemberGroupsForMember(username); return result.Select(x => x.Id).Distinct(); } } public IEnumerable GetMembersInRole(string roleName) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); return _memberRepository.GetByMemberGroup(roleName); } } public IEnumerable FindMembersInRole(string roleName, string usernameToMatch, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { scope.ReadLock(Constants.Locks.MemberTree); return _memberRepository.FindMembersInRole(roleName, usernameToMatch, matchType); } } public bool DeleteRole(string roleName, bool throwIfBeingUsed) { using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MemberTree); if (throwIfBeingUsed) { // get members in role var membersInRole = _memberRepository.GetByMemberGroup(roleName); if (membersInRole.Any()) throw new InvalidOperationException("The role " + roleName + " is currently assigned to members"); } var query = Query().Where(g => g.Name == roleName); var found = _memberGroupRepository.Get(query).ToArray(); foreach (var memberGroup in found) _memberGroupService.Delete(memberGroup); scope.Complete(); return found.Length > 0; } } public void AssignRole(string username, string roleName) { AssignRoles(new[] { username }, new[] { roleName }); } public void AssignRoles(string[] usernames, string[] roleNames) { using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MemberTree); var ids = _memberGroupRepository.GetMemberIds(usernames); _memberGroupRepository.AssignRoles(ids, roleNames); scope.Events.Dispatch(AssignedRoles, this, new RolesEventArgs(ids, roleNames), nameof(AssignedRoles)); scope.Complete(); } } public void DissociateRole(string username, string roleName) { DissociateRoles(new[] { username }, new[] { roleName }); } public void DissociateRoles(string[] usernames, string[] roleNames) { using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MemberTree); var ids = _memberGroupRepository.GetMemberIds(usernames); _memberGroupRepository.DissociateRoles(ids, roleNames); scope.Events.Dispatch(RemovedRoles, this, new RolesEventArgs(ids, roleNames), nameof(RemovedRoles)); scope.Complete(); } } public void AssignRole(int memberId, string roleName) { AssignRoles(new[] { memberId }, new[] { roleName }); } public void AssignRoles(int[] memberIds, string[] roleNames) { using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MemberTree); _memberGroupRepository.AssignRoles(memberIds, roleNames); scope.Events.Dispatch(AssignedRoles, this, new RolesEventArgs(memberIds, roleNames), nameof(AssignedRoles)); scope.Complete(); } } public void DissociateRole(int memberId, string roleName) { DissociateRoles(new[] { memberId }, new[] { roleName }); } public void DissociateRoles(int[] memberIds, string[] roleNames) { using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MemberTree); _memberGroupRepository.DissociateRoles(memberIds, roleNames); scope.Events.Dispatch(RemovedRoles, this, new RolesEventArgs(memberIds, roleNames), nameof(RemovedRoles)); scope.Complete(); } } #endregion #region Private Methods private void Audit(AuditType type, int userId, int objectId, string message = null) { _auditRepository.Save(new AuditItem(objectId, type, userId, ObjectTypes.GetName(UmbracoObjectTypes.Member), message)); } #endregion #region Event Handlers /// /// Occurs before Delete /// public static event TypedEventHandler> Deleting; /// /// Occurs after Delete /// public static event TypedEventHandler> Deleted; /// /// Occurs before Save /// public static event TypedEventHandler> Saving; /// /// Occurs after Save /// public static event TypedEventHandler> Saved; /// /// Occurs after roles have been assigned. /// public static event TypedEventHandler AssignedRoles; /// /// Occurs after roles have been removed. /// public static event TypedEventHandler RemovedRoles; /// /// Occurs after members have been exported. /// public static event TypedEventHandler Exported; #endregion #region Membership /// /// Exports a member. /// /// /// This is internal for now and is used to export a member in the member editor, /// it will raise an event so that auditing logs can be created. /// public MemberExportModel ExportMember(Guid key) { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { var query = Query().Where(x => x.Key == key); var member = _memberRepository.Get(query).FirstOrDefault(); if (member == null) return null; var model = new MemberExportModel { Id = member.Id, Key = member.Key, Name = member.Name, Username = member.Username, Email = member.Email, Groups = GetAllRoles(member.Id).ToList(), ContentTypeAlias = member.ContentTypeAlias, CreateDate = member.CreateDate, UpdateDate = member.UpdateDate, Properties = new List(GetPropertyExportItems(member)) }; scope.Events.Dispatch(Exported, this, new ExportedMemberEventArgs(member, model)); return model; } } private static IEnumerable GetPropertyExportItems(IMember member) { if (member == null) throw new ArgumentNullException(nameof(member)); var exportProperties = new List(); foreach (var property in member.Properties) { var propertyExportModel = new MemberExportProperty { Id = property.Id, Alias = property.Alias, Name = property.PropertyType.Name, Value = property.GetValue(), // TODO: ignoring variants CreateDate = property.CreateDate, UpdateDate = property.UpdateDate }; exportProperties.Add(propertyExportModel); } return exportProperties; } #endregion #region Content Types /// /// Delete Members of the specified MemberType id /// /// Id of the MemberType public void DeleteMembersOfType(int memberTypeId) { // note: no tree to manage here using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.MemberTree); // TODO: What about content that has the contenttype as part of its composition? var query = Query().Where(x => x.ContentTypeId == memberTypeId); var members = _memberRepository.Get(query).ToArray(); var deleteEventArgs = new DeleteEventArgs(members); if (scope.Events.DispatchCancelable(Deleting, this, deleteEventArgs)) { scope.Complete(); return; } foreach (var member in members) { // delete media // triggers the deleted event (and handles the files) DeleteLocked(scope, member); } scope.Complete(); } } private IMemberType GetMemberType(IScope scope, string memberTypeAlias) { if (memberTypeAlias == null) throw new ArgumentNullException(nameof(memberTypeAlias)); if (string.IsNullOrWhiteSpace(memberTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias)); scope.ReadLock(Constants.Locks.MemberTypes); var memberType = _memberTypeRepository.Get(memberTypeAlias); if (memberType == null) throw new Exception($"No MemberType matching the passed in Alias: '{memberTypeAlias}' was found"); // causes rollback return memberType; } private IMemberType GetMemberType(string memberTypeAlias) { if (memberTypeAlias == null) throw new ArgumentNullException(nameof(memberTypeAlias)); if (string.IsNullOrWhiteSpace(memberTypeAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(memberTypeAlias)); using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { return GetMemberType(scope, memberTypeAlias); } } #endregion } }