using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Data.Common; using System.Data.SqlClient; using System.Linq; using Umbraco.Core.Events; using Umbraco.Core.Logging; using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Security; namespace Umbraco.Core.Services { /// /// Represents the UserService, which is an easy access to operations involving , and eventually Backoffice Users. /// public class UserService : RepositoryService, IUserService { //TODO: We need to change the isUpgrading flag to use an app state enum as described here: http://issues.umbraco.org/issue/U4-6816 // in the meantime, we will use a boolean which we are currently using during upgrades to ensure that a user object is not persisted during this phase, otherwise // exceptions can occur if the db is not in it's correct state. internal bool IsUpgrading { get; set; } public UserService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory) : base(provider, repositoryFactory, logger, eventMessagesFactory) { IsUpgrading = false; } #region Implementation of IMembershipUserService /// /// Gets the default MemberType alias /// /// By default we'll return the 'writer', but we need to check it exists. If it doesn't we'll /// return the first type that is not an admin, otherwise if there's only one we will return that one. /// Alias of the default MemberType public string GetDefaultMemberType() { using (var repository = RepositoryFactory.CreateUserTypeRepository(UowProvider.GetUnitOfWork())) { var types = repository.GetAll().Select(x => x.Alias).ToArray(); if (types.Any() == false) { throw new EntityNotFoundException("No member types could be resolved"); } if (types.InvariantContains("writer")) { return types.First(x => x.InvariantEquals("writer")); } if (types.Length == 1) { return types.First(); } //first that is not admin return types.First(x => x.InvariantEquals("admin") == false); } } /// /// Checks if a User with the username exists /// /// Username to check /// True if the User exists otherwise False public bool Exists(string username) { using (var repository = RepositoryFactory.CreateUserRepository(UowProvider.GetUnitOfWork())) { return repository.Exists(username); } } /// /// Creates a new User /// /// The user will be saved in the database and returned with an Id /// Username of the user to create /// Email of the user to create /// which the User should be based on /// public IUser CreateUserWithIdentity(string username, string email, IUserType userType) { return CreateUserWithIdentity(username, email, "", userType); } /// /// Creates and persists a new /// /// 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 /// IUser IMembershipMemberService.CreateWithIdentity(string username, string email, string passwordValue, string memberTypeAlias) { var userType = GetUserTypeByAlias(memberTypeAlias); if (userType == null) { throw new EntityNotFoundException("The user type " + memberTypeAlias + " could not be resolved"); } return CreateUserWithIdentity(username, email, passwordValue, userType); } /// /// 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 /// 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 IUser CreateUserWithIdentity(string username, string email, string passwordValue, IUserType userType) { if (userType == null) throw new ArgumentNullException("userType"); //TODO: PUT lock here!! var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateUserRepository(uow)) { var loginExists = uow.Database.ExecuteScalar("SELECT COUNT(id) FROM umbracoUser WHERE userLogin = @Login", new { Login = username }) != 0; if (loginExists) throw new ArgumentException("Login already exists"); var user = new User(userType) { DefaultToLiveEditing = false, Email = email, Language = Configuration.GlobalSettings.DefaultUILanguage, Name = username, RawPasswordValue = passwordValue, Username = username, StartContentId = -1, StartMediaId = -1, IsLockedOut = false, IsApproved = true }; //adding default sections content and media user.AddAllowedSection("content"); user.AddAllowedSection("media"); if (SavingUser.IsRaisedEventCancelled(new SaveEventArgs(user), this)) return user; repository.AddOrUpdate(user); uow.Commit(); SavedUser.RaiseEvent(new SaveEventArgs(user, false), this); return user; } } /// /// Gets a User by its integer id /// /// Id /// public IUser GetById(int id) { using (var repository = RepositoryFactory.CreateUserRepository(UowProvider.GetUnitOfWork())) { var user = repository.Get((int)id); return user; } } /// /// Gets an by its provider key /// /// Id to use for retrieval /// public IUser GetByProviderKey(object id) { var asInt = id.TryConvertTo(); if (asInt.Success) { return GetById((int)id); } return null; } /// /// Get an by email /// /// Email to use for retrieval /// public IUser GetByEmail(string email) { using (var repository = RepositoryFactory.CreateUserRepository(UowProvider.GetUnitOfWork())) { var query = Query.Builder.Where(x => x.Email.Equals(email)); var user = repository.GetByQuery(query).FirstOrDefault(); return user; } } /// /// Get an by username /// /// Username to use for retrieval /// public IUser GetByUsername(string username) { using (var repository = RepositoryFactory.CreateUserRepository(UowProvider.GetUnitOfWork())) { var query = Query.Builder.Where(x => x.Username.Equals(username)); var user = repository.GetByQuery(query).FirstOrDefault(); return user; } } /// /// Deletes an /// /// to Delete public void Delete(IUser membershipUser) { //disable membershipUser.IsApproved = false; //can't rename if it's going to take up too many chars if (membershipUser.Username.Length + 9 <= 125) { membershipUser.Username = DateTime.Now.ToString("yyyyMMdd") + "_" + membershipUser.Username; } Save(membershipUser); } /// /// This is simply a helper method which essentially just wraps the MembershipProvider's ChangePassword method /// /// /// This method exists so that Umbraco developers can use one entry point to create/update users if they choose to. /// /// The user to save the password for /// The password to save public void SavePassword(IUser user, string password) { if (user == null) throw new ArgumentNullException("user"); var provider = MembershipProviderExtensions.GetUsersMembershipProvider(); if (provider.IsUmbracoMembershipProvider() == false) throw new NotSupportedException("When using a non-Umbraco membership provider you must change the user password by using the MembershipProvider.ChangePassword method"); provider.ChangePassword(user.Username, "", password); //go re-fetch the member and update the properties that may have changed var result = GetByUsername(user.Username); if (result != null) { //should never be null but it could have been deleted by another thread. user.RawPasswordValue = result.RawPasswordValue; user.LastPasswordChangeDate = result.LastPasswordChangeDate; user.UpdateDate = user.UpdateDate; } } /// /// Deletes or disables a User /// /// to delete /// True to permanently delete the user, False to disable the user public void Delete(IUser user, bool deletePermanently) { if (deletePermanently == false) { Delete(user); } else { if (DeletingUser.IsRaisedEventCancelled(new DeleteEventArgs(user), this)) return; var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateUserRepository(uow)) { repository.Delete(user); uow.Commit(); } DeletedUser.RaiseEvent(new DeleteEventArgs(user, false), this); } } /// /// Saves an /// /// to Save /// Optional parameter to raise events. /// Default is True otherwise set to False to not raise events public void Save(IUser entity, bool raiseEvents = true) { if (raiseEvents) { if (SavingUser.IsRaisedEventCancelled(new SaveEventArgs(entity), this)) return; } var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateUserRepository(uow)) { repository.AddOrUpdate(entity); try { uow.Commit(); } catch (DbException ex) { //Special case, if we are upgrading and an exception occurs, just continue if (IsUpgrading == false) throw; Logger.WarnWithException("An error occurred attempting to save a user instance during upgrade, normally this warning can be ignored", ex); return; } } if (raiseEvents) SavedUser.RaiseEvent(new SaveEventArgs(entity, false), this); } /// /// Saves a list of objects /// /// to save /// Optional parameter to raise events. /// Default is True otherwise set to False to not raise events public void Save(IEnumerable entities, bool raiseEvents = true) { if (raiseEvents) { if (SavingUser.IsRaisedEventCancelled(new SaveEventArgs(entities), this)) return; } var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateUserRepository(uow)) { foreach (var member in entities) { repository.AddOrUpdate(member); } //commit the whole lot in one go uow.Commit(); } if (raiseEvents) SavedUser.RaiseEvent(new SaveEventArgs(entities, false), this); } /// /// 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, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateUserRepository(uow)) { var query = new 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("matchType"); } return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, dto => dto.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, int pageIndex, int pageSize, out int totalRecords, StringPropertyMatchType matchType = StringPropertyMatchType.StartsWith) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateUserRepository(uow)) { var query = new 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("matchType"); } return repository.GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, dto => dto.Username); } } /// /// Gets the total number of Users 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 Users for passed in type public int GetCount(MemberCountType countType) { using (var repository = RepositoryFactory.CreateUserRepository(UowProvider.GetUnitOfWork())) { IQuery query; switch (countType) { case MemberCountType.All: query = new Query(); return repository.Count(query); case MemberCountType.Online: throw new NotImplementedException(); //var fromDate = DateTime.Now.AddMinutes(-Membership.UserIsOnlineTimeWindow); //query = // Query.Builder.Where( // x => // ((Member)x).PropertyTypeAlias == Constants.Conventions.Member.LastLoginDate && // ((Member)x).DateTimePropertyValue > fromDate); //return repository.GetCountByQuery(query); case MemberCountType.LockedOut: query = Query.Builder.Where( x => x.IsLockedOut); return repository.GetCountByQuery(query); case MemberCountType.Approved: query = Query.Builder.Where( x => x.IsApproved); return repository.GetCountByQuery(query); default: throw new ArgumentOutOfRangeException("countType"); } } } /// /// Gets a list of paged objects /// /// Current page index /// Size of the page /// Total number of records found (out) /// public IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecords) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateUserRepository(uow)) { return repository.GetPagedResultsByQuery(null, pageIndex, pageSize, out totalRecords, member => member.Username); } } #endregion #region Implementation of IUserService /// /// Gets an IProfile by User Id. /// /// Id of the User to retrieve /// public IProfile GetProfileById(int id) { var user = GetUserById(id); return user.ProfileData; } /// /// Gets a profile by username /// /// Username /// public IProfile GetProfileByUserName(string username) { var user = GetByUsername(username); return user.ProfileData; } /// /// Gets a user by Id /// /// Id of the user to retrieve /// public IUser GetUserById(int id) { using (var repository = RepositoryFactory.CreateUserRepository(UowProvider.GetUnitOfWork())) { return repository.Get(id); } } /// /// Replaces the same permission set for a single user to any number of entities /// /// If no 'entityIds' are specified all permissions will be removed for the specified user. /// Id of the user /// Permissions as enumerable list of /// Specify the nodes to replace permissions for. If nothing is specified all permissions are removed. public void ReplaceUserPermissions(int userId, IEnumerable permissions, params int[] entityIds) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateUserRepository(uow)) { repository.ReplaceUserPermissions(userId, permissions, entityIds); } } /// /// Assigns the same permission set for a single user to any number of entities /// /// Id of the user /// /// Specify the nodes to replace permissions for public void AssignUserPermission(int userId, char permission, params int[] entityIds) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateUserRepository(uow)) { repository.AssignUserPermission(userId, permission, entityIds); } } /// /// Gets all UserTypes or thosed specified as parameters /// /// Optional Ids of UserTypes to retrieve /// An enumerable list of public IEnumerable GetAllUserTypes(params int[] ids) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateUserTypeRepository(uow)) { return repository.GetAll(ids); } } /// /// Gets a UserType by its Alias /// /// Alias of the UserType to retrieve /// public IUserType GetUserTypeByAlias(string alias) { using (var repository = RepositoryFactory.CreateUserTypeRepository(UowProvider.GetUnitOfWork())) { var query = Query.Builder.Where(x => x.Alias == alias); var contents = repository.GetByQuery(query); return contents.SingleOrDefault(); } } /// /// Gets a UserType by its Id /// /// Id of the UserType to retrieve /// public IUserType GetUserTypeById(int id) { using (var repository = RepositoryFactory.CreateUserTypeRepository(UowProvider.GetUnitOfWork())) { return repository.Get(id); } } /// /// Gets a UserType by its Name /// /// Name of the UserType to retrieve /// public IUserType GetUserTypeByName(string name) { using (var repository = RepositoryFactory.CreateUserTypeRepository(UowProvider.GetUnitOfWork())) { var query = Query.Builder.Where(x => x.Name == name); var contents = repository.GetByQuery(query); return contents.SingleOrDefault(); } } /// /// Saves a UserType /// /// UserType to save /// Optional parameter to raise events. /// Default is True otherwise set to False to not raise events public void SaveUserType(IUserType userType, bool raiseEvents = true) { if (raiseEvents) { if (SavingUserType.IsRaisedEventCancelled(new SaveEventArgs(userType), this)) return; } var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateUserTypeRepository(uow)) { repository.AddOrUpdate(userType); uow.Commit(); } if (raiseEvents) SavedUserType.RaiseEvent(new SaveEventArgs(userType, false), this); } /// /// Deletes a UserType /// /// UserType to delete public void DeleteUserType(IUserType userType) { if (DeletingUserType.IsRaisedEventCancelled(new DeleteEventArgs(userType), this)) return; var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateUserTypeRepository(uow)) { repository.Delete(userType); uow.Commit(); } DeletedUserType.RaiseEvent(new DeleteEventArgs(userType, false), this); } /// /// Removes a specific section from all users /// /// This is useful when an entire section is removed from config /// Alias of the section to remove public void DeleteSectionFromAllUsers(string sectionAlias) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateUserRepository(uow)) { var assignedUsers = repository.GetUsersAssignedToSection(sectionAlias); foreach (var user in assignedUsers) { //now remove the section for each user and commit user.RemoveAllowedSection(sectionAlias); repository.AddOrUpdate(user); } uow.Commit(); } } /// /// Add a specific section to all users or those specified as parameters /// /// This is useful when a new section is created to allow specific users accessing it /// Alias of the section to add /// Specifiying nothing will add the section to all user public void AddSectionToAllUsers(string sectionAlias, params int[] userIds) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateUserRepository(uow)) { IEnumerable users; if (userIds.Any()) { users = repository.GetAll(userIds); } else { users = repository.GetAll(); } foreach (var user in users.Where(u => !u.AllowedSections.InvariantContains(sectionAlias))) { //now add the section for each user and commit user.AddAllowedSection(sectionAlias); repository.AddOrUpdate(user); } uow.Commit(); } } /// /// Get permissions set for a user and optional node ids /// /// If no permissions are found for a particular entity then the user's default permissions will be applied /// User to retrieve permissions for /// Specifiying nothing will return all user permissions for all nodes /// An enumerable list of public IEnumerable GetPermissions(IUser user, params int[] nodeIds) { var uow = UowProvider.GetUnitOfWork(); using (var repository = RepositoryFactory.CreateUserRepository(uow)) { var explicitPermissions = repository.GetUserPermissionsForEntities(user.Id, nodeIds); //if no permissions are assigned to a particular node then we will fill in those permissions with the user's defaults var result = new List(explicitPermissions); var missingIds = nodeIds.Except(result.Select(x => x.EntityId)); foreach (var id in missingIds) { result.Add( new EntityPermission( user.Id, id, user.DefaultPermissions.ToArray())); } return result; } } #endregion /// /// Occurs before Save /// public static event TypedEventHandler> SavingUser; /// /// Occurs after Save /// public static event TypedEventHandler> SavedUser; /// /// Occurs before Delete /// public static event TypedEventHandler> DeletingUser; /// /// Occurs after Delete /// public static event TypedEventHandler> DeletedUser; /// /// Occurs before Save /// public static event TypedEventHandler> SavingUserType; /// /// Occurs after Save /// public static event TypedEventHandler> SavedUserType; /// /// Occurs before Delete /// public static event TypedEventHandler> DeletingUserType; /// /// Occurs after Delete /// public static event TypedEventHandler> DeletedUserType; } }